diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b3d084c --- /dev/null +++ b/.env.example @@ -0,0 +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/.gitignore b/.gitignore index efa407c..c5d2607 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +trades/* + +.vscode +.pylintrc +.ruff_cache + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -159,4 +165,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/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. diff --git a/README.md b/README.md index e740730..2b05c54 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,68 @@ 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 + +```bash +# Option 1: run as installed package +pump_bot --help + +# Option 2: run directly +python -m src.cli --help +``` + +> **You're all set! 🎉** Now you can start using the bot. Check `--help` for available commands. 🚀 ## Note on limits diff --git a/bots/bot-sniper-1-geyser.yaml b/bots/bot-sniper-1-geyser.yaml new file mode 100644 index 0000000..65b38c2 --- /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: 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: 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..08c24b2 --- /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: 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: 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/buy.py b/buy.py deleted file mode 100644 index 0d39617..0000000 --- a/buy.py +++ /dev/null @@ -1,277 +0,0 @@ -import asyncio -import json -import base64 -import struct -import base58 -import hashlib -import websockets -import time - -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 solders.keypair import Keypair -from solders.instruction import Instruction, AccountMeta -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: - 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(' 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: - print(f"Subscription confirmed") + elif "result" in data: + print("Subscription confirmed") except Exception as e: - print(f"An error occurred: {str(e)}") + print(f"An error occurred: {e!s}") + if __name__ == "__main__": - asyncio.run(listen_for_transactions()) \ No newline at end of file + asyncio.run(listen_for_transactions()) diff --git a/learning-examples/bonding-curve-progress/get_bonding_curve_status.py b/learning-examples/bonding-curve-progress/get_bonding_curve_status.py new file mode 100644 index 0000000..29974f3 --- /dev/null +++ b/learning-examples/bonding-curve-progress/get_bonding_curve_status.py @@ -0,0 +1,156 @@ +""" +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 +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 + +load_dotenv() + +RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") + +# Change to token you want to query +TOKEN_MINT = "xWrzYY4c1LnbSkLrd2LDUg9vw7YtVyJhGmw7MABpump" + +# Constants +PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") +EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(" 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]: + """ + 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) + + +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") + + data = response.value.data + if data[:8] != EXPECTED_DISCRIMINATOR: + raise ValueError("Invalid curve state discriminator") + + return BondingCurveState(data) + + +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, PUMP_PROGRAM_ID + ) + + 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 + ) + + print("\nBonding curve status:") + print("-" * 50) + 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 PumpSwap." + ) + 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() -> 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__": + main() diff --git a/learning-examples/bonding-curve-progress/get_graduating_tokens.py b/learning-examples/bonding-curve-progress/get_graduating_tokens.py new file mode 100644 index 0000000..e6d2540 --- /dev/null +++ b/learning-examples/bonding-curve-progress/get_graduating_tokens.py @@ -0,0 +1,162 @@ +""" +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 +from solana.rpc.types import MemcmpOpts, TokenAccountOpts +from solders.pubkey import Pubkey + +load_dotenv() + +# 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 +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 with real token reserves below a threshold. + + 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 (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: bool = client is None + try: + if should_close_client: + client = AsyncClient(RPC_ENDPOINT, commitment="processed", timeout=180) + await client.is_connected() + + # Define on-chain filters for getProgramAccounts + filters = [ + 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 + response = await client.get_program_accounts( + PUMP_PROGRAM_ID, + encoding="base64", + filters=filters + ) + + result = [] + for acc in response.value: + raw = acc.account.data + + # Extract real_token_reserves (u64 = 8 bytes, little-endian) + offset: int = 24 # real_token_reserves field offset + real_token_reserves: int = 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. + + Args: + data: The token account data as bytes + + Returns: + The mint address as a base58-encoded string + """ + return str(Pubkey(data[:32])) + + +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) + + 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__": + asyncio.run(main()) 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/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]: - """ - Derives the associated bonding curve address for a given mint - """ - return Pubkey.find_program_address( - [ - b"bonding-curve", - bytes(mint) - ], - program_id - ) - -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") - - data = response.value.data - if data[:8] != EXPECTED_DISCRIMINATOR: - raise ValueError("Invalid curve state discriminator") - - 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) - - 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) - - print("\nBonding Curve Status:") - print("-" * 50) - 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("-" * 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') - - args = parser.parse_args() - asyncio.run(check_token_status(args.mint_address)) - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/learning-examples/cleanup_accounts.py b/learning-examples/cleanup_accounts.py new file mode 100644 index 0000000..8809253 --- /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, 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 + 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()) diff --git a/learning-examples/compute_associated_bonding_curve.py b/learning-examples/compute_associated_bonding_curve.py index 3caa9aa..12343ec 100644 --- a/learning-examples/compute_associated_bonding_curve.py +++ b/learning-examples/compute_associated_bonding_curve.py @@ -1,52 +1,52 @@ -import sys import os +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 +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from core.pubkeys import PumpAddresses, SystemAddresses + 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 - + derived_address, _ = Pubkey.find_program_address( [ bytes(bonding_curve), - bytes(TOKEN_PROGRAM_ID), - bytes(mint), + bytes(SystemAddresses.TOKEN_PROGRAM), + bytes(mint), ], - ATA_PROGRAM_ID + SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, ) 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) - + + bonding_curve_address, bump = get_bonding_curve_address( + mint, PumpAddresses.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 +54,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..1bebc96 100644 --- a/learning-examples/decode_from_blockSubscribe.py +++ b/learning-examples/decode_from_blockSubscribe.py @@ -1,59 +1,63 @@ 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 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 + 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') +tx_file_path = "" + +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) -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..c342607 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("learning-examples/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..630d8f3 100644 --- a/learning-examples/decode_from_getTransaction.py +++ b/learning-examples/decode_from_getTransaction.py @@ -1,32 +1,31 @@ import json -import base58 -from solana.transaction import Transaction -from solders.pubkey import Pubkey import struct import sys -import base64 -import sys +import base58 + +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 -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 +33,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: - response = await conn.get_account_info(curve_address) + +async def get_bonding_curve_state( + conn: AsyncClient, curve_address: Pubkey +) -> BondingCurveState: + response = await conn.get_account_info(curve_address, encoding="base64") if not response.value or not response.value.data: raise ValueError("Invalid curve state: No data") @@ -43,11 +48,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 +72,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-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(" 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: {e!s}") + + +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_MIGRATOR_ID + ) + }, + { + "commitment": "confirmed", + "encoding": "json", + "showRewards": False, + "transactionDetails": "full", + "maxSupportedTransactionVersion": 0, + }, + ], + } + ) + + await websocket.send(subscription_message) + response = await websocket.recv() + print(f"Subscription response: {response}") + print("\nListening for Raydium pool initialization events...") + + while True: + 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", [] + ) + + # Check for initialize2 instruction + for log in logs: + if ( + "Program log: initialize2: InitializeInstruction2" + in log + ): + print( + "Found initialize2 instruction!" + ) + process_initialize2_transaction(tx) + break + + except TimeoutError: + print("\nChecking connection...") + print("Connection alive") + continue + + except Exception as e: + print(f"\nConnection error: {e!s}") + print("Retrying in 5 seconds...") + await asyncio.sleep(5) + + +if __name__ == "__main__": + asyncio.run(listen_for_events()) diff --git a/learning-examples/listen-migrations/listen_logsubscribe.py b/learning-examples/listen-migrations/listen_logsubscribe.py new file mode 100644 index 0000000..c69d40c --- /dev/null +++ b/learning-examples/listen-migrations/listen_logsubscribe.py @@ -0,0 +1,169 @@ +""" +Listens for 'Migrate' instructions from a Solana migration program via WebSocket. +Parses and logs transaction details (e.g., mint, liquidity, token accounts) for successful migrations. + +Note: skips transactions with truncated logs (no Program data in the logs -> 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 +import os +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") + + +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(" 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/listen-new-tokens/generated/geyser_pb2_grpc.py b/learning-examples/listen-new-tokens/generated/geyser_pb2_grpc.py new file mode 100644 index 0000000..f56411e --- /dev/null +++ b/learning-examples/listen-new-tokens/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/listen-new-tokens/generated/solana_storage_pb2.py b/learning-examples/listen-new-tokens/generated/solana_storage_pb2.py new file mode 100644 index 0000000..7458363 --- /dev/null +++ b/learning-examples/listen-new-tokens/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/listen-new-tokens/generated/solana_storage_pb2.pyi b/learning-examples/listen-new-tokens/generated/solana_storage_pb2.pyi new file mode 100644 index 0000000..10312a6 --- /dev/null +++ b/learning-examples/listen-new-tokens/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/listen-new-tokens/generated/solana_storage_pb2_grpc.py b/learning-examples/listen-new-tokens/generated/solana_storage_pb2_grpc.py new file mode 100644 index 0000000..1544a78 --- /dev/null +++ b/learning-examples/listen-new-tokens/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-new-tokens/listen_blocksubscribe.py b/learning-examples/listen-new-tokens/listen_blocksubscribe.py new file mode 100644 index 0000000..6f683d5 --- /dev/null +++ b/learning-examples/listen-new-tokens/listen_blocksubscribe.py @@ -0,0 +1,161 @@ +""" +Listens to Solana blocks for Pump.fun 'create' instructions via WebSocket. +Decodes transaction data to extract mint, bonding curve, and user details. + +It is usually slower than other listeners. +""" + +import asyncio +import base64 +import json +import os +import struct + +import websockets +from dotenv import load_dotenv +from solders.pubkey import Pubkey +from solders.transaction import VersionedTransaction + +load_dotenv() + +WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") +PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") + + +def load_idl(file_path): + with open(file_path) 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(" 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(" Pubkey: + """ + Find the associated bonding curve for a given mint and bonding curve. + This uses the standard ATA derivation. + """ + derived_address, _ = Pubkey.find_program_address( + [ + bytes(bonding_curve), + bytes(TOKEN_PROGRAM_ID), + bytes(mint), + ], + ASSOCIATED_TOKEN_PROGRAM_ID, + ) + return derived_address + + +def parse_create_instruction(data): + if len(data) < 8: + return None + offset = 8 + parsed_data = {} + + # Parse fields based on CreateEvent structure + fields = [ + ("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(" 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/listen-new-tokens/proto/solana-storage.proto b/learning-examples/listen-new-tokens/proto/solana-storage.proto new file mode 100644 index 0000000..56e3eb0 --- /dev/null +++ b/learning-examples/listen-new-tokens/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/learning-examples/listen_create_from_blocksubscribe.py b/learning-examples/listen_create_from_blocksubscribe.py deleted file mode 100644 index d3d726a..0000000 --- a/learning-examples/listen_create_from_blocksubscribe.py +++ /dev/null @@ -1,109 +0,0 @@ -import asyncio -import json -import websockets -import base64 -import struct -import hashlib -import sys -import os -from solders.transaction import VersionedTransaction -from solders.pubkey import Pubkey - -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: - 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: - """ - Find the associated bonding curve for a given mint and bonding curve. - This uses the standard ATA derivation. - """ - derived_address, _ = Pubkey.find_program_address( - [ - bytes(bonding_curve), - bytes(TOKEN_PROGRAM_ID), - bytes(mint), - ], - ATA_PROGRAM_ID - ) - return derived_address - -# Load the IDL JSON file -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') - -def parse_create_instruction(data): - if len(data) < 8: - return None - offset = 8 - parsed_data = {} - - # Parse fields based on CreateEvent structure - fields = [ - ('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 - } - ] - }) - - await websocket.send(subscription_message) - response = await websocket.recv() - print(f"Subscription response: {response}") - print("\nListening for Raydium pool initialization events...") - - while True: - 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', []) - - # Check for initialize2 instruction - for log in logs: - 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 diff --git a/learning-examples/manual_buy.py b/learning-examples/manual_buy.py index 2482f5b..a4850b8 100644 --- a/learning-examples/manual_buy.py +++ b/learning-examples/manual_buy.py @@ -1,29 +1,25 @@ 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 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.message import Message +from solders.pubkey import Pubkey +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: - response = await conn.get_account_info(curve_address) + +async def get_pump_curve_state( + conn: AsyncClient, curve_address: Pubkey +) -> BondingCurveState: + response = await conn.get_account_info(curve_address, encoding="base64") if not response.value or not response.value.data: raise ValueError("Invalid curve state: No data") @@ -69,14 +73,25 @@ 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): - private_key = base58.b58decode("ENTER_PRIVATE_KEY") + +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(os.environ.get("SOLANA_PRIVATE_KEY")) payer = Keypair.from_bytes(private_key) async with AsyncClient(RPC_ENDPOINT) as client: @@ -94,38 +109,54 @@ async def buy_token(mint: Pubkey, bonding_curve: Pubkey, associated_bonding_curv # 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})...") + 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: {e!s}" + ) 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 +166,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: - response = await conn.get_account_info(curve_address) + +async def get_pump_curve_state( + conn: AsyncClient, curve_address: Pubkey +) -> BondingCurveState: + response = await conn.get_account_info(curve_address, encoding="base64") if not response.value or not response.value.data: raise ValueError("Invalid curve state: No data") @@ -61,11 +69,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 +85,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): - private_key = base58.b58decode("SOLANA_PRIVATE_KEY") + +async def sell_token( + mint: Pubkey, + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + slippage: float = 0.25, + max_retries=5, +): + private_key = base58.b58decode(os.environ.get("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 +117,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,29 +128,56 @@ 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(" 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 # Offset where the base_mint field is stored in the account data structure + filters = [ + MemcmpOpts(offset=offset, bytes=base_mint_bytes) + ] + + response = await client.get_program_accounts( + amm_program_id, + encoding="base64", + 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) -> 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(market_address, encoding="base64") + data = response.value.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"), + ("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: + """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: 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 + """ + 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=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_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) -> 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 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 with slippage protection + 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") + + # Define all accounts needed for the sell instruction + accounts = [ + 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=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), + AccountMeta(pubkey=user_quote_token_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=pool_base_token_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=pool_quote_token_account, is_signer=False, is_writable=True), + AccountMeta(pubkey=PUMP_PROTOCOL_FEE_RECIPIENT, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_PROTOCOL_FEE_RECIPIENT_TOKEN_ACCOUNT, is_signer=False, is_writable=True), + AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), + AccountMeta(pubkey=SYSTEM_TOKEN_PROGRAM, is_signer=False, is_writable=False), + 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=PUMP_SWAP_EVENT_AUTHORITY, is_signer=False, is_writable=False), + AccountMeta(pubkey=PUMP_AMM_PROGRAM_ID, is_signer=False, is_writable=False), + ] + + data = SELL_DISCRIMINATOR + struct.pack("=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", + "grpcio>=1.71.0", + "grpcio-tools>=1.71.0", + "protobuf>=5.29.4", + "pyyaml>=6.0.2", + "uvloop>=0.21.0", +] + +[project.optional-dependencies] +dev = [ + "ruff>=0.10.0" +] + +[project.scripts] +pump_bot = "bot_runner: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", + "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" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 1cb1aab..0000000 --- a/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -base58>=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 diff --git a/sell.py b/sell.py deleted file mode 100644 index f35efb2..0000000 --- a/sell.py +++ /dev/null @@ -1,141 +0,0 @@ -import asyncio -import json -import base64 -import struct -import base58 -from typing import Final - -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 solders.keypair import Keypair -from solders.instruction import Instruction, AccountMeta -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 * - -# Here and later all the discriminators are precalculated. See learning-examples/discriminator.py -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: - 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(" 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/__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..2d4dca8 --- /dev/null +++ b/src/cleanup/manager.py @@ -0,0 +1,106 @@ +import asyncio + +from solders.pubkey import Pubkey +from spl.token.instructions import BurnParams, CloseAccountParams, burn, close_account + +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 + +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, + force_burn: bool = False, + ): + """ + Args: + client: Solana RPC client + wallet: Wallet for signing transactions + """ + self.client = client + 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: + """ + 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() + + priority_fee = ( + await self.priority_fee_manager.calculate_priority_fee([ata]) + if self.use_priority_fee + 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: + logger.info(f"ATA {ata} does not exist or already closed.") + return + + balance = await self.client.get_token_account_balance(ata) + instructions = [] + + if balance > 0 and self.close_with_force_burn: + 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, + ) + ) + instructions.append(burn_ix) + + 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 + + # Include close account instruction + 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, + ) + ) + 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/cleanup/modes.py b/src/cleanup/modes.py new file mode 100644 index 0000000..428f3b4 --- /dev/null +++ b/src/cleanup/modes.py @@ -0,0 +1,42 @@ +from cleanup.manager import AccountCleanupManager +from utils.logger import get_logger + +logger = get_logger(__name__) + + +def should_cleanup_after_failure(cleanup_mode) -> bool: + return cleanup_mode == "on_fail" + + +def should_cleanup_after_sell(cleanup_mode) -> bool: + return cleanup_mode == "after_sell" + + +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, 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_prior_fee, force_burn) + await manager.cleanup_ata(mint) + +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_prior_fee, force_burn) + await manager.cleanup_ata(mint) + +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_prior_fee, force_burn) + for mint in mints: + await manager.cleanup_ata(mint) diff --git a/src/config_loader.py b/src/config_loader.py new file mode 100644 index 0000000..15be4a5 --- /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, override=True) + else: + # If not found relative to config, try relative to current working directory + load_dotenv(env_file, override=True) + + 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/__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..5868d4d --- /dev/null +++ b/src/core/client.py @@ -0,0 +1,240 @@ +""" +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 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 +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 + +from 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 + self._cached_blockhash: Hash | None = None + self._blockhash_lock = asyncio.Lock() + 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.""" + 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}") + finally: + 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. + + 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 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 + + 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. + + 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, encoding="base64") # base64 encoding for account data by default + 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) -> Hash: + """Get the latest blockhash. + + Returns: + Recent blockhash as string + """ + client = await self.get_client() + response = await client.get_latest_blockhash(commitment="processed") + return response.value.blockhash + + async def build_and_send_transaction( + self, + instructions: list[Instruction], + signer_keypair: Keypair, + skip_preflight: bool = True, + max_retries: int = 3, + priority_fee: int | None = None, + ) -> str: + """ + Send a transaction with optional priority fee. + + Args: + 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 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(72_000), # Default compute unit limit + set_compute_unit_price(priority_fee), + ] + instructions = fee_instructions + instructions + + recent_blockhash = await self.get_cached_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=Processed + ) + response = await client.send_transaction(transaction, 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: {e!s}, 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, sleep_seconds=1) + return True + except Exception as 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: + """ + 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: {e!s}", exc_info=True) + return None + except json.JSONDecodeError as e: + logger.error(f"Failed to decode RPC response: {e!s}", exc_info=True) + return None diff --git a/src/core/curve.py b/src/core/curve.py new file mode 100644 index 0000000..2ffaa5c --- /dev/null +++ b/src/core/curve.py @@ -0,0 +1,135 @@ +""" +Bonding curve operations for pump.fun tokens. +""" + +import struct +from typing import Final + +from construct import Flag, Int64ul, Struct +from solders.pubkey import Pubkey + +from core.client import SolanaClient +from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS +from 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/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..b04235d --- /dev/null +++ b/src/core/priority_fee/dynamic_fee.py @@ -0,0 +1,69 @@ +import statistics + +from solders.pubkey import Pubkey + +from core.client import SolanaClient +from core.priority_fee import PriorityFeePlugin +from utils.logger import get_logger + +logger = get_logger(__name__) + + +class DynamicPriorityFee(PriorityFeePlugin): + """Dynamic priority fee plugin using getRecentPrioritizationFees.""" + + 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, accounts: list[Pubkey] | None = None + ) -> int | None: + """ + 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]: Median priority fee in microlamports, or None if the request fails. + """ + try: + 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)}", exc_info=True + ) + 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..64102bc --- /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 microlamports. + """ + 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 microlamports, 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..8497cbb --- /dev/null +++ b/src/core/priority_fee/manager.py @@ -0,0 +1,92 @@ +from solders.pubkey import Pubkey + +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__) + + +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 microlamports. + extra_fee: Percentage increase to apply to the base fee. + hard_cap: Maximum allowed priority fee in microlamports. + """ + 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, 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 microlamports, or None if no fee should be applied. + """ + base_fee = await self._get_base_fee(accounts) + 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, accounts: list[Pubkey] | None = None) -> int | None: + """ + Determine the base fee based on the configuration. + + Returns: + 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(accounts) + 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 priority fee if both are disabled + return None 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/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/__init__.py b/src/monitoring/__init__.py new file mode 100644 index 0000000..e69de29 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/block_event_processor.py b/src/monitoring/block_event_processor.py new file mode 100644 index 0000000..a7e540d --- /dev/null +++ b/src/monitoring/block_event_processor.py @@ -0,0 +1,168 @@ +""" +Event processing for pump.fun tokens. +""" + +import base64 +import json +import struct +from typing import Any + +from solders.pubkey import Pubkey +from solders.transaction import VersionedTransaction + +from trading.base import TokenInfo +from 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") 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) -> TokenInfo | None: + """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})" + ) + + 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: {e!s}") + 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", # base64 is faster than other encoding options + "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) + try: + pong_waiter = await websocket.ping() + await asyncio.wait_for(pong_waiter, timeout=10) + except 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: {e!s}") + + async def _wait_for_token_creation(self, websocket) -> TokenInfo | None: + """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 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: {e!s}") + + return None 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/monitoring/logs_event_processor.py b/src/monitoring/logs_event_processor.py new file mode 100644 index 0000000..9aff061 --- /dev/null +++ b/src/monitoring/logs_event_processor.py @@ -0,0 +1,151 @@ +""" +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) + 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/__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..2359590 --- /dev/null +++ b/src/trading/base.py @@ -0,0 +1,101 @@ +""" +Base interfaces for trading operations. +""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any + +from solders.pubkey import Pubkey + +from core.pubkeys import PumpAddresses + + +@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: str | None = None + error_message: str | None = None + amount: float | None = None + price: float | None = 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 + + 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 new file mode 100644 index 0000000..c0b2a2b --- /dev/null +++ b/src/trading/buyer.py @@ -0,0 +1,218 @@ +""" +Buy operations for pump.fun tokens. +""" + +import struct +from typing import Final + +from solders.instruction import AccountMeta, Instruction +from solders.pubkey import Pubkey +from spl.token.instructions import create_idempotent_associated_token_account + +from core.client import SolanaClient +from core.curve import BondingCurveManager +from core.priority_fee.manager import PriorityFeeManager +from core.pubkeys import ( + LAMPORTS_PER_SOL, + TOKEN_DECIMALS, + PumpAddresses, + SystemAddresses, +) +from core.wallet import Wallet +from trading.base import TokenInfo, Trader, TradeResult +from utils.logger import get_logger + +logger = get_logger(__name__) + +# Discriminator for the buy instruction +EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(" TradeResult: + """Execute buy operation. + + Args: + token_info: Token information + + Returns: + TradeResult with buy outcome + """ + try: + # Convert amount to lamports + amount_lamports = int(self.amount * LAMPORTS_PER_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)) + + associated_token_account = self.wallet.get_associated_token_address( + token_info.mint + ) + + tx_signature = await self._send_buy_transaction( + token_info, + associated_token_account, + token_amount, + 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: + 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: {e!s}") + return TradeResult(success=False, error_message=str(e)) + + async def _send_buy_transaction( + self, + token_info: TokenInfo, + associated_token_account: Pubkey, + token_amount: float, + max_amount_lamports: int, + ) -> str: + """Send buy transaction. + + Args: + 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 + + 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=token_info.mint, is_signer=False, is_writable=False), + AccountMeta( + 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 + ), + 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 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 = ( + EXPECTED_DISCRIMINATOR + + struct.pack(" TradeResult: + """Execute sell operation. + + Args: + token_info: Token information + + Returns: + TradeResult with sell outcome + """ + try: + # Get associated token account + associated_token_account = self.wallet.get_associated_token_address( + token_info.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( + token_info.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( + token_info, + 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, + token_info: TokenInfo, + associated_token_account: Pubkey, + token_amount: int, + min_sol_output: int, + ) -> str: + """Send sell transaction. + + Args: + 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 + + 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=token_info.mint, is_signer=False, is_writable=False), + AccountMeta( + 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 + ), + 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 + data = ( + EXPECTED_DISCRIMINATOR + + struct.pack(" None: + """Start the trading bot and listen for new tokens.""" + logger.info("Starting pump.fun trader") + 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") + + 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: + # 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}") + + finally: + await self._cleanup_resources() + logger.info("Pump trader has shut down") + + 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] = monotonic() + 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: + logger.debug(f"Token {token_info.symbol} already processed. Skipping...") + return + + # Record timestamp when token was discovered + 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})") + + async def _process_token_queue(self) -> None: + """Continuously process tokens from the queue, only if they're fresh.""" + while True: + try: + token_info = await self.token_queue.get() + token_key = str(token_info.mint) + + # Check if token is still "fresh" + current_time = monotonic() + 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)" + ) + 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() + + async def _handle_token( + self, token_info: TokenInfo + ) -> None: + """Handle a new token creation event. + + Args: + token_info: Token information + """ + try: + # 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..." + ) + 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: + await self._handle_successful_buy(token_info, buy_result) + else: + await self._handle_failed_buy(token_info, buy_result) + + # Only wait for next token in yolo mode + if self.yolo_mode: + logger.info( + f"YOLO mode enabled. Waiting {self.wait_time_before_new_token} seconds before looking for next 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 _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 + """ + 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)) + + 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, + action: str, + token_info: TokenInfo, + price: float, + amount: float, + tx_hash: str | None, + ) -> None: + """Log trade information. + + Args: + action: Trade action (buy/sell) + token_info: Token information + price: Token price in SOL + amount: Trade amount in SOL + tx_hash: Transaction hash + """ + 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) if tx_hash else None, + } + + 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/__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..0c0cf90 --- /dev/null +++ b/src/utils/logger.py @@ -0,0 +1,58 @@ +""" +Logging utilities for the pump.fun trading bot. +""" + +import logging + +# 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) + + _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() + + # 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", + ) + + file_handler = logging.FileHandler(filename) + file_handler.setLevel(level) + file_handler.setFormatter(formatter) + + root_logger.addHandler(file_handler) diff --git a/tests/compare_listeners.py b/tests/compare_listeners.py new file mode 100644 index 0000000..2c6b0bd --- /dev/null +++ b/tests/compare_listeners.py @@ -0,0 +1,208 @@ +""" +Test script to compare BlockListener, LogsListener, and GeyserListener +Runs all listeners simultaneously to compare their performance +""" + +import asyncio +import logging +import os +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" +) +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 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 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 + + 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") + + 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: + # 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") + # No need for explicit cancellation as gather() will be interrupted + + for i, callback in enumerate(callbacks): + logger.info(f"{listener_names[i]} detected {len(callback.detected_tokens)} tokens") + + # 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 + + # 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)}") + + 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, 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) + + # 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__": + 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..1be17f8 --- /dev/null +++ b/tests/test_block_listener.py @@ -0,0 +1,101 @@ +""" +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 + +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" +) +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 = 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 new file mode 100644 index 0000000..e27e1b2 --- /dev/null +++ b/tests/test_logs_listener.py @@ -0,0 +1,101 @@ +""" +Test script for LogsListener +Tests websocket monitoring for new pump.fun tokens using logsSubscribe +""" + +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.logs_listener import LogsListener +from trading.base import TokenInfo + +load_dotenv() + +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger("logs-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_logs_listener( + match_string: str | None = None, + creator_address: str | None = None, + test_duration: int = 60, +): + """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 = LogsListener(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 = 30 + + logger.info("Starting logs listener test (using logsSubscribe)") + asyncio.run(test_logs_listener(match_string, creator_address, test_duration)) diff --git a/trade.py b/trade.py deleted file mode 100644 index 80bb438..0000000 --- a/trade.py +++ /dev/null @@ -1,157 +0,0 @@ -import asyncio -import json -import base64 -import struct -import base58 -import hashlib -import websockets -import os -import argparse -from datetime import datetime - -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 solders.keypair import Keypair -from solders.instruction import Instruction, AccountMeta -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 * - -# Import functions from buy.py -from buy import get_pump_curve_state, calculate_pump_curve_price, buy_token, listen_for_create_transaction - -# Import functions from sell.py -from sell import sell_token - -def log_trade(action, token_data, price, tx_hash): - os.makedirs("trades", exist_ok=True) - log_entry = { - "timestamp": datetime.utcnow().isoformat(), - "action": action, - "token_address": token_data['mint'], - "price": price, - "tx_hash": tx_hash - } - with open("trades/trades.log", 'a') as log_file: - json.dump(log_entry, log_file) - log_file.write("\n") - -async def trade(websocket=None, match_string=None, bro_address=None, marry_mode=False, yolo_mode=False): - if websocket is None: - async with websockets.connect(WSS_ENDPOINT) as websocket: - await _trade(websocket, match_string, bro_address, marry_mode, yolo_mode) - else: - await _trade(websocket, match_string, bro_address, marry_mode, yolo_mode) - -async def _trade(websocket, match_string=None, bro_address=None, marry_mode=False, yolo_mode=False): - while True: - print("Waiting for a new token creation...") - token_data = await listen_for_create_transaction(websocket) - print("New token created:") - print(json.dumps(token_data, indent=2)) - - if match_string and not (match_string.lower() in token_data['name'].lower() or match_string.lower() in token_data['symbol'].lower()): - print(f"Token does not match the criteria '{match_string}'. Skipping...") - if not yolo_mode: - break - continue - - if bro_address and token_data['user'] != bro_address: - print(f"Token not created by the specified user '{bro_address}'. Skipping...") - if not yolo_mode: - break - continue - - # Save token information to a .txt file in the "trades" directory - mint_address = token_data['mint'] - os.makedirs("trades", exist_ok=True) - file_name = os.path.join("trades", f"{mint_address}.txt") - with open(file_name, 'w') as file: - file.write(json.dumps(token_data, indent=2)) - print(f"Token information saved to {file_name}") - - print("Waiting for 15 seconds for things to stabilize...") - await asyncio.sleep(15) - - mint = Pubkey.from_string(token_data['mint']) - bonding_curve = Pubkey.from_string(token_data['bondingCurve']) - associated_bonding_curve = Pubkey.from_string(token_data['associatedBondingCurve']) - - # Fetch the token price - async with AsyncClient(RPC_ENDPOINT) as client: - curve_state = await get_pump_curve_state(client, bonding_curve) - token_price_sol = calculate_pump_curve_price(curve_state) - - print(f"Bonding curve address: {bonding_curve}") - print(f"Token price: {token_price_sol:.10f} SOL") - print(f"Buying {BUY_AMOUNT:.6f} SOL worth of the new token with {BUY_SLIPPAGE*100:.1f}% slippage tolerance...") - buy_tx_hash = await buy_token(mint, bonding_curve, associated_bonding_curve, BUY_AMOUNT, BUY_SLIPPAGE) - if buy_tx_hash: - log_trade("buy", token_data, token_price_sol, str(buy_tx_hash)) - else: - print("Buy transaction failed.") - - if not marry_mode: - print("Waiting for 20 seconds before selling...") - await asyncio.sleep(20) - - print(f"Selling tokens with {SELL_SLIPPAGE*100:.1f}% slippage tolerance...") - sell_tx_hash = await sell_token(mint, bonding_curve, associated_bonding_curve, SELL_SLIPPAGE) - if sell_tx_hash: - log_trade("sell", token_data, token_price_sol, str(sell_tx_hash)) - else: - print("Sell transaction failed or no tokens to sell.") - else: - print("Marry mode enabled. Skipping sell operation.") - - if not yolo_mode: - break - -async def main(yolo_mode=False, match_string=None, bro_address=None, marry_mode=False): - if yolo_mode: - while True: - try: - async with websockets.connect(WSS_ENDPOINT) as websocket: - while True: - try: - await trade(websocket, match_string, bro_address, marry_mode, yolo_mode) - except websockets.exceptions.ConnectionClosed: - print("WebSocket connection closed. Reconnecting...") - break - except Exception as e: - print(f"An error occurred: {e}") - print("Waiting for 5 seconds before looking for the next token...") - await asyncio.sleep(5) - except Exception as e: - print(f"Connection error: {e}") - print("Reconnecting in 5 seconds...") - await asyncio.sleep(5) - else: - # For non-YOLO mode, create a websocket connection and close it after one trade - async with websockets.connect(WSS_ENDPOINT) as websocket: - await trade(websocket, match_string, bro_address, marry_mode, yolo_mode) - -async def ping_websocket(websocket): - while True: - try: - await websocket.ping() - await asyncio.sleep(20) # Send a ping every 20 seconds - except: - break - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Trade tokens on Solana.") - 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") - args = parser.parse_args() - asyncio.run(main(yolo_mode=args.yolo, match_string=args.match, bro_address=args.bro, marry_mode=args.marry)) \ No newline at end of file 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 03d0e64..d226794 100644 --- a/trades/trades.log +++ b/trades/trades.log @@ -1,2 +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-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"}