diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3371f76 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,157 @@ +# Pump Bot Development Guide + +This is a trading bot for pump.fun and letsbonk.fun platforms that snipes new tokens and implements various trading strategies. + +## Project Structure + +- `src/` - Main source code +- `learning-examples/` - Educational scripts and examples +- `bots/` - Bot configuration files (YAML) +- `logs/` - Log files from bot executions +- `idl/` - Interface definition files for Solana programs + +## Bash Commands & Development + +### Setup Commands +```bash +# Install dependencies +uv sync + +# Activate virtual environment (Unix/macOS) +source .venv/bin/activate + +# Install as editable package +uv pip install -e . +``` + +### Running the Bot +```bash +# Run as installed package +pump_bot + +# Run directly +uv run src/bot_runner.py +``` + +### Learning Examples +```bash +# Bonding curve status +uv run learning-examples/bonding-curve-progress/get_bonding_curve_status.py TOKEN_ADDRESS + +# Listen to migrations +uv run learning-examples/listen-migrations/listen_logsubscribe.py +uv run learning-examples/listen-migrations/listen_blocksubscribe_old_raydium.py + +# Compute associated bonding curve +uv run learning-examples/compute_associated_bonding_curve.py + +# Listen to new tokens +uv run learning-examples/listen-new-tokens/listen_logsubscribe_abc.py +uv run learning-examples/listen-new-tokens/compare_listeners.py +``` + +### Code Quality +```bash +# Format code +ruff format + +# Lint code +ruff check + +# Fix linting issues +ruff check --fix +``` + +## Code Style & Conventions + +### Python Style (Ruff Configuration) +- **Line length**: 88 characters +- **Indentation**: 4 spaces +- **Target Python**: 3.11+ +- **Quote style**: Double quotes +- **Import sorting**: Enabled + +### Linting Rules +- Security best practices (S) +- Type annotations (ANN) +- Exception handling (BLE, TRY) +- Code complexity (C90) +- Pylint conventions (PL) +- No commented-out code (ERA) + +### Code Organization +- **Imports**: Standard library, third-party, local imports +- **Docstrings**: Google-style for functions and classes +- **Type hints**: Required for all public functions +- **Logging**: Use `get_logger(__name__)` pattern +- **Error handling**: Comprehensive try-catch with proper logging + +### File Structure Patterns +- `__init__.py` files for all packages +- Separate concerns: client, trading, monitoring, platforms +- Abstract base classes in `interfaces/` +- Platform-specific implementations in `platforms/` + +## Workflow & Development Practices + +### Configuration Management +- Environment variables in `.env` file +- Bot configurations in YAML files under `bots/` +- Platform detection from config files +- Validation of platform-listener combinations + +### Logging +- Timestamped log files in `logs/` directory +- Format: `{bot_name}_{timestamp}.log` +- Different log levels for development vs production +- Centralized logger utility in `utils/logger.py` + +### Trading Architecture +- Universal trader pattern for platform abstraction +- Platform-specific implementations (pumpfun, letsbonk) +- Position tracking and management +- Priority fee management (dynamic/fixed) + +### Monitoring Systems +- Multiple listener types: logs, blocks, geyser, pumpportal +- Universal listeners with platform abstraction +- Event parsing and processing +- Real-time data stream handling + +### Development Workflow +1. Make changes to source code +2. Run `ruff check --fix` for linting +3. Run `ruff format` for formatting +4. Test with learning examples (standalone scripts) before deploying bots +5. Use separate processes for production bot instances + +### Bot Configuration +- YAML-based configuration files +- Environment variable interpolation +- Platform-specific settings +- Trading parameters (slippage, amounts, timeouts) +- Filter configurations for token selection +- Cleanup and account management settings + +### Testing Strategy +- Learning examples serve as integration tests +- Manual testing with learning scripts +- Configuration validation before bot startup +- Logging verification for debugging + +## Key Features + +- **Multi-platform support**: pump.fun and letsbonk.fun +- **Multiple listening methods**: WebSocket logs, block subscription, Geyser +- **Trading strategies**: Time-based, take profit/stop loss, manual +- **Priority fee management**: Dynamic and fixed fee strategies +- **Account cleanup**: Automated token account management +- **Extreme fast mode**: Skip validation for faster execution + +## Security Considerations + +- Private keys stored in environment variables +- No sensitive data in configuration files +- Comprehensive input validation +- Error handling to prevent crashes +- Rate limiting and retry mechanisms \ No newline at end of file diff --git a/learning-examples/bonding-curve-progress/get_bonding_curve_status.py b/learning-examples/bonding-curve-progress/get_bonding_curve_status.py index 9e18a1e..e00a5c5 100644 --- a/learning-examples/bonding-curve-progress/get_bonding_curve_status.py +++ b/learning-examples/bonding-curve-progress/get_bonding_curve_status.py @@ -25,14 +25,16 @@ RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") TOKEN_MINT = "..." # Constants -PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") +PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string( + "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" +) EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(" 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) """ @@ -100,14 +103,14 @@ async def get_bonding_curve_state( ) -> 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 """ @@ -125,7 +128,7 @@ async def get_bonding_curve_state( async def check_token_status(mint_address: str) -> None: """ Checks and prints the status of a token and its bonding curve. - + Args: mint_address: The token mint address as a string """ @@ -174,9 +177,11 @@ async def check_token_status(mint_address: str) -> None: 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", nargs='?', help="The token mint address", default=TOKEN_MINT) + parser.add_argument( + "mint_address", nargs="?", help="The token mint address", default=TOKEN_MINT + ) args = parser.parse_args() - + asyncio.run(check_token_status(args.mint_address)) diff --git a/learning-examples/bonding-curve-progress/get_graduating_tokens.py b/learning-examples/bonding-curve-progress/get_graduating_tokens.py index e6d2540..f97f708 100644 --- a/learning-examples/bonding-curve-progress/get_graduating_tokens.py +++ b/learning-examples/bonding-curve-progress/get_graduating_tokens.py @@ -20,8 +20,12 @@ 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") +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") @@ -30,10 +34,10 @@ BONDING_CURVE_DISCRIMINATOR_BYTES: Final[bytes] = bytes.fromhex("17b7f83760d8ac6 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 """ @@ -47,19 +51,21 @@ async def get_bonding_curves_by_reserves(client: AsyncClient | None = None) -> l 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=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 + PUMP_PROGRAM_ID, encoding="base64", filters=filters ) result = [] @@ -68,7 +74,7 @@ async def get_bonding_curves_by_reserves(client: AsyncClient | None = None) -> l # Extract real_token_reserves (u64 = 8 bytes, little-endian) offset: int = 24 # real_token_reserves field offset - real_token_reserves: int = struct.unpack(" dict | None: """ Find the SPL token account owned by a bonding curve. - + Args: bonding_curve_address: The bonding curve public key (as a string) client: Optional AsyncClient instance. If None, a new one will be created. - + Returns: The associated SPL token account data or None if not found """ @@ -101,12 +107,12 @@ async def find_associated_bonding_curve( if should_close_client: client = AsyncClient(RPC_ENDPOINT) await client.is_connected() - + response = await client.get_token_accounts_by_owner( Pubkey.from_string(bonding_curve_address), - TokenAccountOpts(program_id=TOKEN_PROGRAM_ID) + TokenAccountOpts(program_id=TOKEN_PROGRAM_ID), ) - + if response.value and len(response.value) > 0: return response.value[0].account else: @@ -123,10 +129,10 @@ async def find_associated_bonding_curve( 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 """ @@ -137,7 +143,7 @@ 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) @@ -147,13 +153,13 @@ async def main() -> None: 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 diff --git a/learning-examples/bonding-curve-progress/poll_bonding_curve_progress.py b/learning-examples/bonding-curve-progress/poll_bonding_curve_progress.py index c9e35d1..1c8b1c9 100644 --- a/learning-examples/bonding-curve-progress/poll_bonding_curve_progress.py +++ b/learning-examples/bonding-curve-progress/poll_bonding_curve_progress.py @@ -17,21 +17,25 @@ 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") +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 """ @@ -41,14 +45,14 @@ def get_associated_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> Pu 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 """ @@ -62,13 +66,13 @@ async def get_account_data(client: AsyncClient, pubkey: Pubkey) -> bytes: 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 """ @@ -89,7 +93,7 @@ def parse_curve_state(data: bytes) -> dict: def print_curve_status(state: dict) -> None: """ Print the current status of the bonding curve in a readable format. - + Args: state: The parsed bonding curve state dictionary """ @@ -98,11 +102,11 @@ def print_curve_status(state: dict) -> None: progress = 100.0 else: # Pump.fun constants (already converted to human-readable format) - TOTAL_SUPPLY = 1_000_000_000 # 1B tokens + TOTAL_SUPPLY = 1_000_000_000 # 1B tokens RESERVED_TOKENS = 206_900_000 # 206.9M tokens reserved for migration - + initial_real_token_reserves = TOTAL_SUPPLY - RESERVED_TOKENS # 793.1M tokens - + if initial_real_token_reserves > 0: left_tokens = state["real_token_reserves"] progress = 100 - (left_tokens * 100) / initial_real_token_reserves @@ -124,7 +128,9 @@ async def track_curve() -> None: return mint_pubkey: Pubkey = Pubkey.from_string(TOKEN_MINT) - curve_pubkey: Pubkey = get_associated_bonding_curve_address(mint_pubkey, PUMP_PROGRAM_ID) + 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") diff --git a/learning-examples/cleanup_accounts.py b/learning-examples/cleanup_accounts.py index 8809253..7e6d383 100644 --- a/learning-examples/cleanup_accounts.py +++ b/learning-examples/cleanup_accounts.py @@ -20,11 +20,15 @@ PRIVATE_KEY = os.getenv("SOLANA_PRIVATE_KEY") MINT_ADDRESS = Pubkey.from_string("9WHpYbqG6LJvfCYfMjvGbyo1wHXgroCrixPb33s2pump") -async def close_account_if_exists(client: SolanaClient, wallet: Wallet, account: Pubkey, mint: Pubkey): +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 + info = await solana_client.get_account_info( + account, encoding="base64" + ) # base64 encoding for account data by deafult # WARNING: This will permanently burn all tokens in the account before closing it # Closing account is impossible if balance is positive diff --git a/learning-examples/compute_associated_bonding_curve.py b/learning-examples/compute_associated_bonding_curve.py index 7a2aa1b..c2eba7a 100644 --- a/learning-examples/compute_associated_bonding_curve.py +++ b/learning-examples/compute_associated_bonding_curve.py @@ -7,6 +7,7 @@ SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" ) + def get_bonding_curve_address(mint: Pubkey, program_id: Pubkey) -> tuple[Pubkey, int]: """ Derives the bonding curve address for a given mint @@ -37,9 +38,7 @@ def main(): try: mint = Pubkey.from_string(mint_address) - bonding_curve_address, bump = get_bonding_curve_address( - mint, PUMP_PROGRAM - ) + bonding_curve_address, bump = get_bonding_curve_address(mint, PUMP_PROGRAM) # Calculate the associated bonding curve associated_bonding_curve = find_associated_bonding_curve( diff --git a/learning-examples/decode_from_blockSubscribe.py b/learning-examples/decode_from_blockSubscribe.py index 5b53ff6..838a910 100644 --- a/learning-examples/decode_from_blockSubscribe.py +++ b/learning-examples/decode_from_blockSubscribe.py @@ -77,7 +77,9 @@ def decode_transaction(tx_data, idl): print(f"\nInstruction {idx}:") print(f"Program ID: {program_id}") - if program_id == "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P": # Pump Fun Program + if ( + program_id == "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + ): # Pump Fun Program ix_data = bytes(ix.data) discriminator = struct.unpack(" 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 = " |" @@ -206,7 +244,7 @@ class DetectionTracker: 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} |" @@ -214,14 +252,16 @@ class DetectionTracker: 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 + 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} |" @@ -232,10 +272,11 @@ class DetectionTracker: # ============ 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"} @@ -251,10 +292,10 @@ async def fetch_existing_market_pubkeys(): "filters": [ {"dataSize": MARKET_ACCOUNT_LENGTH}, {"memcmp": {"offset": 0, "bytes": MARKET_DISCRIMINATOR}}, - {"memcmp": {"offset": 75, "bytes": QUOTE_MINT_SOL}} - ] - } - ] + {"memcmp": {"offset": 75, "bytes": QUOTE_MINT_SOL}}, + ], + }, + ], } async with aiohttp.ClientSession() as session: @@ -266,7 +307,7 @@ async def fetch_existing_market_pubkeys(): 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 = {} @@ -287,15 +328,19 @@ def parse_market_account_data(data): try: for field_name, field_type in fields: if field_type == "pubkey": - value = data[offset:offset + 32] + 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(" 0 else 0 print(f"{provider:<22} | {count:<16} | {percentage:.1f}%") - + # Calculate average latency between providers self._print_provider_latency_matrix() - + def _print_provider_latency_matrix(self): """Print a matrix of average latency between providers""" # Get unique providers all_providers = set() for token_data in self.tokens.values(): - all_providers.update(token_data['detections'].keys()) - + all_providers.update(token_data["detections"].keys()) + if len(all_providers) <= 1: return - + providers_list = sorted(all_providers) - + # Calculate column width based on longest provider name max_provider_len = max(len(provider) for provider in providers_list) col_width = max(max_provider_len, 8) # Minimum 8 for latency values - + print("\nAverage Latency Matrix (ms):") - + # Print header header = f"{'':>{col_width}} |" for provider in providers_list: header += f" {provider:>{col_width}} |" print(header) print("-" * len(header)) - + # Calculate and print latency matrix for provider1 in providers_list: row = f"{provider1:>{col_width}} |" @@ -174,15 +186,17 @@ class DetectionTracker: if provider1 == provider2: row += f" {'—':>{col_width}} |" continue - + # Calculate average latency latencies = [] for token_data in self.tokens.values(): - detections = token_data['detections'] + detections = token_data["detections"] if provider1 in detections and provider2 in detections: - latency_ms = (detections[provider2] - detections[provider1]) * 1000 + latency_ms = ( + detections[provider2] - detections[provider1] + ) * 1000 latencies.append(latency_ms) - + if latencies: avg_latency = sum(latencies) / len(latencies) row += f" {avg_latency:>+{col_width}.1f} |" @@ -193,6 +207,7 @@ class DetectionTracker: # ============ TOKEN DETECTION METHODS ============ + async def fetch_existing_token_mints(): """ Fetch existing token mints to avoid duplicate detections @@ -208,31 +223,33 @@ def parse_create_instruction(data): """ if len(data) < 8: return None - + offset = 8 # Skip discriminator parsed_data = {} try: # Parse name (string) - length = struct.unpack(" 0: try: - mint = str(transaction.message.account_keys[ix.accounts[0]]) # First account is usually the mint - + mint = str( + transaction.message.account_keys[ + ix.accounts[0] + ] + ) # First account is usually the mint + if mint in known_tokens: continue ts = time.time() - tracker.add_token(mint, parsed["name"], parsed["symbol"], - f"{provider_name}_block", ts) + tracker.add_token( + mint, + parsed["name"], + parsed["symbol"], + f"{provider_name}_block", + ts, + ) known_tokens.add(mint) except Exception as e: - print(f"[ERROR] Failed to process block instruction: {e}") + print( + f"[ERROR] Failed to process block instruction: {e}" + ) except Exception as e: print(f"[ERROR] Failed to process transaction: {e}") - + except Exception as e: print(f"[ERROR] Block listener for {provider_name}: {e}") except Exception as e: - print(f"[ERROR] Connection error in block listener for {provider_name}: {e}") + print( + f"[ERROR] Connection error in block listener for {provider_name}: {e}" + ) print("[INFO] Reconnecting in 5 seconds...") await asyncio.sleep(5) @@ -350,20 +392,22 @@ async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens """ if known_tokens is None: known_tokens = set() - + while True: try: print(f"[INFO] Connecting logs listener to {provider_name}...") async with websockets.connect(wss_url) as websocket: - subscription_message = json.dumps({ - "jsonrpc": "2.0", - "id": 1, - "method": "logsSubscribe", - "params": [ - {"mentions": [str(PUMP_PROGRAM_ID)]}, - {"commitment": "processed"}, - ], - }) + subscription_message = json.dumps( + { + "jsonrpc": "2.0", + "id": 1, + "method": "logsSubscribe", + "params": [ + {"mentions": [str(PUMP_PROGRAM_ID)]}, + {"commitment": "processed"}, + ], + } + ) await websocket.send(subscription_message) await websocket.recv() print(f"[INFO] Logs listener active for {provider_name}") @@ -379,8 +423,10 @@ async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens log_data = data["params"]["result"]["value"] logs = log_data.get("logs", []) - - if not any("Program log: Instruction: Create" in log for log in logs): + + if not any( + "Program log: Instruction: Create" in log for log in logs + ): continue for log in logs: @@ -401,17 +447,17 @@ async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens ts = time.time() tracker.add_token( - mint, - parsed.get("name", "Unknown"), - parsed.get("symbol", "UNK"), - f"{provider_name}_logs", - ts + mint, + parsed.get("name", "Unknown"), + parsed.get("symbol", "UNK"), + f"{provider_name}_logs", + ts, ) known_tokens.add(mint) break except Exception as e: print(f"[ERROR] Failed to decode Program data: {e}") - + except Exception as e: print(f"[ERROR] Logs listener for {provider_name}: {e}") break @@ -422,7 +468,9 @@ async def listen_logs_subscription(wss_url, provider_name, tracker, known_tokens await asyncio.sleep(5) -async def listen_geyser_grpc(endpoint, api_token, provider_name, tracker, known_tokens=None): +async def listen_geyser_grpc( + endpoint, api_token, provider_name, tracker, known_tokens=None +): """ Listen for new tokens via Geyser gRPC API """ @@ -430,12 +478,14 @@ async def listen_geyser_grpc(endpoint, api_token, provider_name, tracker, known_ # Import the generated protobuf modules from generated import geyser_pb2, geyser_pb2_grpc except ImportError: - print("[ERROR] Could not import geyser_pb2 or geyser_pb2_grpc. Make sure to generate from .proto files") + print( + "[ERROR] Could not import geyser_pb2 or geyser_pb2_grpc. Make sure to generate from .proto files" + ) return - + if known_tokens is None: known_tokens = set() - + while True: try: print(f"[INFO] Connecting Geyser gRPC listener to {provider_name}...") @@ -446,55 +496,70 @@ async def listen_geyser_grpc(endpoint, api_token, provider_name, tracker, known_ ) else: auth = grpc.metadata_call_credentials( - lambda context, callback: callback((("authorization", f"Basic {api_token}"),), None) + lambda context, callback: callback( + (("authorization", f"Basic {api_token}"),), None + ) ) - creds = grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth) + creds = grpc.composite_channel_credentials( + grpc.ssl_channel_credentials(), auth + ) channel = grpc.aio.secure_channel(endpoint, creds) stub = geyser_pb2_grpc.GeyserStub(channel) request = geyser_pb2.SubscribeRequest() - request.transactions["pump_filter"].account_include.append(str(PUMP_PROGRAM_ID)) + request.transactions["pump_filter"].account_include.append( + str(PUMP_PROGRAM_ID) + ) request.transactions["pump_filter"].failed = False request.commitment = geyser_pb2.CommitmentLevel.PROCESSED - + print(f"[INFO] Geyser gRPC listener active for {provider_name}") - + async for update in stub.Subscribe(iter([request])): tracker.increment_messages(provider_name) - + # Skip non-transaction updates if not update.HasField("transaction"): continue - + tx = update.transaction.transaction.transaction msg = getattr(tx, "message", None) if msg is None: continue - + for ix in msg.instructions: if not ix.data.startswith(PUMP_CREATE_PREFIX): continue - + parsed = parse_create_instruction(ix.data) if not parsed: continue - + if len(ix.accounts) == 0 or ix.accounts[0] >= len(msg.account_keys): continue - mint = base58.b58encode(bytes(msg.account_keys[ix.accounts[0]])).decode() + mint = base58.b58encode( + bytes(msg.account_keys[ix.accounts[0]]) + ).decode() if mint in known_tokens: continue ts = time.time() - tracker.add_token(mint, parsed["name"], parsed["symbol"], - f"{provider_name}_geyser", ts) + tracker.add_token( + mint, + parsed["name"], + parsed["symbol"], + f"{provider_name}_geyser", + ts, + ) known_tokens.add(mint) except Exception as e: - print(f"[ERROR] Connection error in Geyser gRPC listener for {provider_name}: {e}") + print( + f"[ERROR] Connection error in Geyser gRPC listener for {provider_name}: {e}" + ) print("[INFO] Reconnecting in 5 seconds...") await asyncio.sleep(5) @@ -505,13 +570,15 @@ async def listen_pumpportal(provider_name, tracker, known_tokens=None): """ if known_tokens is None: known_tokens = set() - + while True: try: print("[INFO] Connecting to PumpPortal WebSocket...") async with websockets.connect(PUMPPORTAL_WS_URL) as websocket: # Subscribe to new token events - await websocket.send(json.dumps({"method": "subscribeNewToken", "params": []})) + await websocket.send( + json.dumps({"method": "subscribeNewToken", "params": []}) + ) print(f"[INFO] PumpPortal listener active for {provider_name}") while True: @@ -520,51 +587,55 @@ async def listen_pumpportal(provider_name, tracker, known_tokens=None): message = await websocket.recv() data = json.loads(message) tracker.increment_messages(provider_name) - + # Extract token information token_info = None if "method" in data and data["method"] == "newToken": token_info = data.get("params", [{}])[0] elif "signature" in data and "mint" in data: token_info = data - + if not token_info: continue - + # Get token details mint = token_info.get("mint") name = token_info.get("name", "Unknown") symbol = token_info.get("symbol", "UNK") - + if not mint: continue - + # Skip known tokens if mint in known_tokens: continue - + # Record the token detection ts = time.time() - tracker.add_token(mint, name, symbol, - f"{provider_name}_pumpportal", ts) + tracker.add_token( + mint, name, symbol, f"{provider_name}_pumpportal", ts + ) known_tokens.add(mint) - + except Exception as e: print(f"[ERROR] PumpPortal listener for {provider_name}: {e}") break except Exception as e: - print(f"[ERROR] Connection error in PumpPortal listener for {provider_name}: {e}") + print( + f"[ERROR] Connection error in PumpPortal listener for {provider_name}: {e}" + ) print("[INFO] Reconnecting in 5 seconds...") await asyncio.sleep(5) # ============ MAIN TEST RUNNER ============ + async def run_comparison_test(providers, test_duration=600): """ Run the comparison test with multiple WebSocket endpoints - + Args: providers: Dict of {provider_name: {'wss': wss_url, 'geyser': (endpoint, api_token)}} test_duration: How long to run the test in seconds (default: 10 minutes) @@ -575,32 +646,38 @@ async def run_comparison_test(providers, test_duration=600): print(f"[INFO] Loaded {len(known_tokens)} existing tokens") tasks = [] - + # Start all listeners for each provider for provider_name, urls in providers.items(): - if urls.get('wss'): + if urls.get("wss"): print(f"[INFO] Starting block listener for {provider_name}") task = asyncio.create_task( - listen_block_subscription(urls['wss'], provider_name, tracker, known_tokens.copy()) + listen_block_subscription( + urls["wss"], provider_name, tracker, known_tokens.copy() + ) ) tasks.append(task) - if urls.get('wss'): + if urls.get("wss"): print(f"[INFO] Starting logs listener for {provider_name}") task = asyncio.create_task( - listen_logs_subscription(urls['wss'], provider_name, tracker, known_tokens.copy()) + listen_logs_subscription( + urls["wss"], provider_name, tracker, known_tokens.copy() + ) ) tasks.append(task) - - if urls.get('geyser'): - endpoint, api_token = urls['geyser'] + + if urls.get("geyser"): + endpoint, api_token = urls["geyser"] if endpoint and api_token: print(f"[INFO] Starting Geyser gRPC listener for {provider_name}") task = asyncio.create_task( - listen_geyser_grpc(endpoint, api_token, provider_name, tracker, known_tokens.copy()) + listen_geyser_grpc( + endpoint, api_token, provider_name, tracker, known_tokens.copy() + ) ) tasks.append(task) - + # Start PumpPortal listener (only once, not per provider) print("[INFO] Starting PumpPortal listener") task = asyncio.create_task( @@ -613,7 +690,7 @@ async def run_comparison_test(providers, test_duration=600): for task in tasks: task.cancel() - + await asyncio.gather(*tasks, return_exceptions=True) tracker.print_summary() @@ -622,21 +699,26 @@ if __name__ == "__main__": # Read providers from environment variables providers = { "provider_1": { - 'wss': os.environ.get("SOLANA_NODE_WSS_ENDPOINT"), - 'geyser': ( + "wss": os.environ.get("SOLANA_NODE_WSS_ENDPOINT"), + "geyser": ( os.environ.get("GEYSER_ENDPOINT"), - os.environ.get("GEYSER_API_TOKEN") - ) + os.environ.get("GEYSER_API_TOKEN"), + ), }, # Add more providers to .env as needed } - + # Filter out any providers with missing endpoints - providers = {name: urls for name, urls in providers.items() - if (urls.get('wss')) or - ('geyser' in urls and urls['geyser'][0] and urls['geyser'][1])} - - print(f"[INFO] Starting Pump.fun token detector comparison test for {TEST_DURATION} seconds") + providers = { + name: urls + for name, urls in providers.items() + if (urls.get("wss")) + or ("geyser" in urls and urls["geyser"][0] and urls["geyser"][1]) + } + + print( + f"[INFO] Starting Pump.fun token detector comparison test for {TEST_DURATION} seconds" + ) print(f"[INFO] Providers: {', '.join(providers.keys())}") - - asyncio.run(run_comparison_test(providers, test_duration=TEST_DURATION)) \ No newline at end of file + + asyncio.run(run_comparison_test(providers, test_duration=TEST_DURATION)) diff --git a/learning-examples/listen-new-tokens/generated/geyser_pb2.py b/learning-examples/listen-new-tokens/generated/geyser_pb2.py index 4db6702..a2b1539 100644 --- a/learning-examples/listen-new-tokens/generated/geyser_pb2.py +++ b/learning-examples/listen-new-tokens/generated/geyser_pb2.py @@ -3,6 +3,7 @@ # 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 @@ -10,131 +11,131 @@ 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' + _runtime_version.Domain.PUBLIC, 5, 29, 0, "", "geyser.proto" ) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() - from generated.solana_storage_pb2 import * -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0cgeyser.proto\x12\x06geyser\x1a\x14solana-storage.proto\"\xf6\t\n\x10SubscribeRequest\x12\x38\n\x08\x61\x63\x63ounts\x18\x01 \x03(\x0b\x32&.geyser.SubscribeRequest.AccountsEntry\x12\x32\n\x05slots\x18\x02 \x03(\x0b\x32#.geyser.SubscribeRequest.SlotsEntry\x12@\n\x0ctransactions\x18\x03 \x03(\x0b\x32*.geyser.SubscribeRequest.TransactionsEntry\x12M\n\x13transactions_status\x18\n \x03(\x0b\x32\x30.geyser.SubscribeRequest.TransactionsStatusEntry\x12\x34\n\x06\x62locks\x18\x04 \x03(\x0b\x32$.geyser.SubscribeRequest.BlocksEntry\x12=\n\x0b\x62locks_meta\x18\x05 \x03(\x0b\x32(.geyser.SubscribeRequest.BlocksMetaEntry\x12\x32\n\x05\x65ntry\x18\x08 \x03(\x0b\x32#.geyser.SubscribeRequest.EntryEntry\x12\x30\n\ncommitment\x18\x06 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x12\x46\n\x13\x61\x63\x63ounts_data_slice\x18\x07 \x03(\x0b\x32).geyser.SubscribeRequestAccountsDataSlice\x12/\n\x04ping\x18\t \x01(\x0b\x32\x1c.geyser.SubscribeRequestPingH\x01\x88\x01\x01\x1aW\n\rAccountsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x35\n\x05value\x18\x02 \x01(\x0b\x32&.geyser.SubscribeRequestFilterAccounts:\x02\x38\x01\x1aQ\n\nSlotsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x32\n\x05value\x18\x02 \x01(\x0b\x32#.geyser.SubscribeRequestFilterSlots:\x02\x38\x01\x1a_\n\x11TransactionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x39\n\x05value\x18\x02 \x01(\x0b\x32*.geyser.SubscribeRequestFilterTransactions:\x02\x38\x01\x1a\x65\n\x17TransactionsStatusEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x39\n\x05value\x18\x02 \x01(\x0b\x32*.geyser.SubscribeRequestFilterTransactions:\x02\x38\x01\x1aS\n\x0b\x42locksEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x33\n\x05value\x18\x02 \x01(\x0b\x32$.geyser.SubscribeRequestFilterBlocks:\x02\x38\x01\x1a[\n\x0f\x42locksMetaEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x37\n\x05value\x18\x02 \x01(\x0b\x32(.geyser.SubscribeRequestFilterBlocksMeta:\x02\x38\x01\x1aQ\n\nEntryEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x32\n\x05value\x18\x02 \x01(\x0b\x32#.geyser.SubscribeRequestFilterEntry:\x02\x38\x01\x42\r\n\x0b_commitmentB\x07\n\x05_ping\"\xbf\x01\n\x1eSubscribeRequestFilterAccounts\x12\x0f\n\x07\x61\x63\x63ount\x18\x02 \x03(\t\x12\r\n\x05owner\x18\x03 \x03(\t\x12=\n\x07\x66ilters\x18\x04 \x03(\x0b\x32,.geyser.SubscribeRequestFilterAccountsFilter\x12#\n\x16nonempty_txn_signature\x18\x05 \x01(\x08H\x00\x88\x01\x01\x42\x19\n\x17_nonempty_txn_signature\"\xf3\x01\n$SubscribeRequestFilterAccountsFilter\x12\x44\n\x06memcmp\x18\x01 \x01(\x0b\x32\x32.geyser.SubscribeRequestFilterAccountsFilterMemcmpH\x00\x12\x12\n\x08\x64\x61tasize\x18\x02 \x01(\x04H\x00\x12\x1d\n\x13token_account_state\x18\x03 \x01(\x08H\x00\x12H\n\x08lamports\x18\x04 \x01(\x0b\x32\x34.geyser.SubscribeRequestFilterAccountsFilterLamportsH\x00\x42\x08\n\x06\x66ilter\"y\n*SubscribeRequestFilterAccountsFilterMemcmp\x12\x0e\n\x06offset\x18\x01 \x01(\x04\x12\x0f\n\x05\x62ytes\x18\x02 \x01(\x0cH\x00\x12\x10\n\x06\x62\x61se58\x18\x03 \x01(\tH\x00\x12\x10\n\x06\x62\x61se64\x18\x04 \x01(\tH\x00\x42\x06\n\x04\x64\x61ta\"m\n,SubscribeRequestFilterAccountsFilterLamports\x12\x0c\n\x02\x65q\x18\x01 \x01(\x04H\x00\x12\x0c\n\x02ne\x18\x02 \x01(\x04H\x00\x12\x0c\n\x02lt\x18\x03 \x01(\x04H\x00\x12\x0c\n\x02gt\x18\x04 \x01(\x04H\x00\x42\x05\n\x03\x63mp\"Y\n\x1bSubscribeRequestFilterSlots\x12!\n\x14\x66ilter_by_commitment\x18\x01 \x01(\x08H\x00\x88\x01\x01\x42\x17\n\x15_filter_by_commitment\"\xd2\x01\n\"SubscribeRequestFilterTransactions\x12\x11\n\x04vote\x18\x01 \x01(\x08H\x00\x88\x01\x01\x12\x13\n\x06\x66\x61iled\x18\x02 \x01(\x08H\x01\x88\x01\x01\x12\x16\n\tsignature\x18\x05 \x01(\tH\x02\x88\x01\x01\x12\x17\n\x0f\x61\x63\x63ount_include\x18\x03 \x03(\t\x12\x17\n\x0f\x61\x63\x63ount_exclude\x18\x04 \x03(\t\x12\x18\n\x10\x61\x63\x63ount_required\x18\x06 \x03(\tB\x07\n\x05_voteB\t\n\x07_failedB\x0c\n\n_signature\"\xd9\x01\n\x1cSubscribeRequestFilterBlocks\x12\x17\n\x0f\x61\x63\x63ount_include\x18\x01 \x03(\t\x12!\n\x14include_transactions\x18\x02 \x01(\x08H\x00\x88\x01\x01\x12\x1d\n\x10include_accounts\x18\x03 \x01(\x08H\x01\x88\x01\x01\x12\x1c\n\x0finclude_entries\x18\x04 \x01(\x08H\x02\x88\x01\x01\x42\x17\n\x15_include_transactionsB\x13\n\x11_include_accountsB\x12\n\x10_include_entries\"\"\n SubscribeRequestFilterBlocksMeta\"\x1d\n\x1bSubscribeRequestFilterEntry\"C\n!SubscribeRequestAccountsDataSlice\x12\x0e\n\x06offset\x18\x01 \x01(\x04\x12\x0e\n\x06length\x18\x02 \x01(\x04\"\"\n\x14SubscribeRequestPing\x12\n\n\x02id\x18\x01 \x01(\x05\"\x85\x04\n\x0fSubscribeUpdate\x12\x0f\n\x07\x66ilters\x18\x01 \x03(\t\x12\x31\n\x07\x61\x63\x63ount\x18\x02 \x01(\x0b\x32\x1e.geyser.SubscribeUpdateAccountH\x00\x12+\n\x04slot\x18\x03 \x01(\x0b\x32\x1b.geyser.SubscribeUpdateSlotH\x00\x12\x39\n\x0btransaction\x18\x04 \x01(\x0b\x32\".geyser.SubscribeUpdateTransactionH\x00\x12\x46\n\x12transaction_status\x18\n \x01(\x0b\x32(.geyser.SubscribeUpdateTransactionStatusH\x00\x12-\n\x05\x62lock\x18\x05 \x01(\x0b\x32\x1c.geyser.SubscribeUpdateBlockH\x00\x12+\n\x04ping\x18\x06 \x01(\x0b\x32\x1b.geyser.SubscribeUpdatePingH\x00\x12+\n\x04pong\x18\t \x01(\x0b\x32\x1b.geyser.SubscribeUpdatePongH\x00\x12\x36\n\nblock_meta\x18\x07 \x01(\x0b\x32 .geyser.SubscribeUpdateBlockMetaH\x00\x12-\n\x05\x65ntry\x18\x08 \x01(\x0b\x32\x1c.geyser.SubscribeUpdateEntryH\x00\x42\x0e\n\x0cupdate_oneof\"o\n\x16SubscribeUpdateAccount\x12\x33\n\x07\x61\x63\x63ount\x18\x01 \x01(\x0b\x32\".geyser.SubscribeUpdateAccountInfo\x12\x0c\n\x04slot\x18\x02 \x01(\x04\x12\x12\n\nis_startup\x18\x03 \x01(\x08\"\xc8\x01\n\x1aSubscribeUpdateAccountInfo\x12\x0e\n\x06pubkey\x18\x01 \x01(\x0c\x12\x10\n\x08lamports\x18\x02 \x01(\x04\x12\r\n\x05owner\x18\x03 \x01(\x0c\x12\x12\n\nexecutable\x18\x04 \x01(\x08\x12\x12\n\nrent_epoch\x18\x05 \x01(\x04\x12\x0c\n\x04\x64\x61ta\x18\x06 \x01(\x0c\x12\x15\n\rwrite_version\x18\x07 \x01(\x04\x12\x1a\n\rtxn_signature\x18\x08 \x01(\x0cH\x00\x88\x01\x01\x42\x10\n\x0e_txn_signature\"\x94\x01\n\x13SubscribeUpdateSlot\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x13\n\x06parent\x18\x02 \x01(\x04H\x00\x88\x01\x01\x12\'\n\x06status\x18\x03 \x01(\x0e\x32\x17.geyser.CommitmentLevel\x12\x17\n\ndead_error\x18\x04 \x01(\tH\x01\x88\x01\x01\x42\t\n\x07_parentB\r\n\x0b_dead_error\"g\n\x1aSubscribeUpdateTransaction\x12;\n\x0btransaction\x18\x01 \x01(\x0b\x32&.geyser.SubscribeUpdateTransactionInfo\x12\x0c\n\x04slot\x18\x02 \x01(\x04\"\xd8\x01\n\x1eSubscribeUpdateTransactionInfo\x12\x11\n\tsignature\x18\x01 \x01(\x0c\x12\x0f\n\x07is_vote\x18\x02 \x01(\x08\x12?\n\x0btransaction\x18\x03 \x01(\x0b\x32*.solana.storage.ConfirmedBlock.Transaction\x12\x42\n\x04meta\x18\x04 \x01(\x0b\x32\x34.solana.storage.ConfirmedBlock.TransactionStatusMeta\x12\r\n\x05index\x18\x05 \x01(\x04\"\xa1\x01\n SubscribeUpdateTransactionStatus\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x0f\n\x07is_vote\x18\x03 \x01(\x08\x12\r\n\x05index\x18\x04 \x01(\x04\x12<\n\x03\x65rr\x18\x05 \x01(\x0b\x32/.solana.storage.ConfirmedBlock.TransactionError\"\xa0\x04\n\x14SubscribeUpdateBlock\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x11\n\tblockhash\x18\x02 \x01(\t\x12\x37\n\x07rewards\x18\x03 \x01(\x0b\x32&.solana.storage.ConfirmedBlock.Rewards\x12@\n\nblock_time\x18\x04 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.UnixTimestamp\x12@\n\x0c\x62lock_height\x18\x05 \x01(\x0b\x32*.solana.storage.ConfirmedBlock.BlockHeight\x12\x13\n\x0bparent_slot\x18\x07 \x01(\x04\x12\x18\n\x10parent_blockhash\x18\x08 \x01(\t\x12\"\n\x1a\x65xecuted_transaction_count\x18\t \x01(\x04\x12<\n\x0ctransactions\x18\x06 \x03(\x0b\x32&.geyser.SubscribeUpdateTransactionInfo\x12\x1d\n\x15updated_account_count\x18\n \x01(\x04\x12\x34\n\x08\x61\x63\x63ounts\x18\x0b \x03(\x0b\x32\".geyser.SubscribeUpdateAccountInfo\x12\x15\n\rentries_count\x18\x0c \x01(\x04\x12-\n\x07\x65ntries\x18\r \x03(\x0b\x32\x1c.geyser.SubscribeUpdateEntry\"\xe2\x02\n\x18SubscribeUpdateBlockMeta\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x11\n\tblockhash\x18\x02 \x01(\t\x12\x37\n\x07rewards\x18\x03 \x01(\x0b\x32&.solana.storage.ConfirmedBlock.Rewards\x12@\n\nblock_time\x18\x04 \x01(\x0b\x32,.solana.storage.ConfirmedBlock.UnixTimestamp\x12@\n\x0c\x62lock_height\x18\x05 \x01(\x0b\x32*.solana.storage.ConfirmedBlock.BlockHeight\x12\x13\n\x0bparent_slot\x18\x06 \x01(\x04\x12\x18\n\x10parent_blockhash\x18\x07 \x01(\t\x12\"\n\x1a\x65xecuted_transaction_count\x18\x08 \x01(\x04\x12\x15\n\rentries_count\x18\t \x01(\x04\"\x9d\x01\n\x14SubscribeUpdateEntry\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\r\n\x05index\x18\x02 \x01(\x04\x12\x12\n\nnum_hashes\x18\x03 \x01(\x04\x12\x0c\n\x04hash\x18\x04 \x01(\x0c\x12\"\n\x1a\x65xecuted_transaction_count\x18\x05 \x01(\x04\x12\"\n\x1astarting_transaction_index\x18\x06 \x01(\x04\"\x15\n\x13SubscribeUpdatePing\"!\n\x13SubscribeUpdatePong\x12\n\n\x02id\x18\x01 \x01(\x05\"\x1c\n\x0bPingRequest\x12\r\n\x05\x63ount\x18\x01 \x01(\x05\"\x1d\n\x0cPongResponse\x12\r\n\x05\x63ount\x18\x01 \x01(\x05\"\\\n\x19GetLatestBlockhashRequest\x12\x30\n\ncommitment\x18\x01 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x42\r\n\x0b_commitment\"^\n\x1aGetLatestBlockhashResponse\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x11\n\tblockhash\x18\x02 \x01(\t\x12\x1f\n\x17last_valid_block_height\x18\x03 \x01(\x04\"X\n\x15GetBlockHeightRequest\x12\x30\n\ncommitment\x18\x01 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x42\r\n\x0b_commitment\".\n\x16GetBlockHeightResponse\x12\x14\n\x0c\x62lock_height\x18\x01 \x01(\x04\"Q\n\x0eGetSlotRequest\x12\x30\n\ncommitment\x18\x01 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x42\r\n\x0b_commitment\"\x1f\n\x0fGetSlotResponse\x12\x0c\n\x04slot\x18\x01 \x01(\x04\"\x13\n\x11GetVersionRequest\"%\n\x12GetVersionResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\"m\n\x17IsBlockhashValidRequest\x12\x11\n\tblockhash\x18\x01 \x01(\t\x12\x30\n\ncommitment\x18\x02 \x01(\x0e\x32\x17.geyser.CommitmentLevelH\x00\x88\x01\x01\x42\r\n\x0b_commitment\"7\n\x18IsBlockhashValidResponse\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\r\n\x05valid\x18\x02 \x01(\x08*\x83\x01\n\x0f\x43ommitmentLevel\x12\r\n\tPROCESSED\x10\x00\x12\r\n\tCONFIRMED\x10\x01\x12\r\n\tFINALIZED\x10\x02\x12\x18\n\x14\x46IRST_SHRED_RECEIVED\x10\x03\x12\r\n\tCOMPLETED\x10\x04\x12\x10\n\x0c\x43REATED_BANK\x10\x05\x12\x08\n\x04\x44\x45\x41\x44\x10\x06\x32\x93\x04\n\x06Geyser\x12\x44\n\tSubscribe\x12\x18.geyser.SubscribeRequest\x1a\x17.geyser.SubscribeUpdate\"\x00(\x01\x30\x01\x12\x33\n\x04Ping\x12\x13.geyser.PingRequest\x1a\x14.geyser.PongResponse\"\x00\x12]\n\x12GetLatestBlockhash\x12!.geyser.GetLatestBlockhashRequest\x1a\".geyser.GetLatestBlockhashResponse\"\x00\x12Q\n\x0eGetBlockHeight\x12\x1d.geyser.GetBlockHeightRequest\x1a\x1e.geyser.GetBlockHeightResponse\"\x00\x12<\n\x07GetSlot\x12\x16.geyser.GetSlotRequest\x1a\x17.geyser.GetSlotResponse\"\x00\x12W\n\x10IsBlockhashValid\x12\x1f.geyser.IsBlockhashValidRequest\x1a .geyser.IsBlockhashValidResponse\"\x00\x12\x45\n\nGetVersion\x12\x19.geyser.GetVersionRequest\x1a\x1a.geyser.GetVersionResponse\"\x00\x42;Z9github.com/rpcpool/yellowstone-grpc/examples/golang/protoP\x00\x62\x06proto3') +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) +_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 + _globals["DESCRIPTOR"]._loaded_options = None + _globals[ + "DESCRIPTOR" + ]._serialized_options = ( + b"Z9github.com/rpcpool/yellowstone-grpc/examples/golang/proto" + ) + _globals["_SUBSCRIBEREQUEST_ACCOUNTSENTRY"]._loaded_options = None + _globals["_SUBSCRIBEREQUEST_ACCOUNTSENTRY"]._serialized_options = b"8\001" + _globals["_SUBSCRIBEREQUEST_SLOTSENTRY"]._loaded_options = None + _globals["_SUBSCRIBEREQUEST_SLOTSENTRY"]._serialized_options = b"8\001" + _globals["_SUBSCRIBEREQUEST_TRANSACTIONSENTRY"]._loaded_options = None + _globals["_SUBSCRIBEREQUEST_TRANSACTIONSENTRY"]._serialized_options = b"8\001" + _globals["_SUBSCRIBEREQUEST_TRANSACTIONSSTATUSENTRY"]._loaded_options = None + _globals["_SUBSCRIBEREQUEST_TRANSACTIONSSTATUSENTRY"]._serialized_options = b"8\001" + _globals["_SUBSCRIBEREQUEST_BLOCKSENTRY"]._loaded_options = None + _globals["_SUBSCRIBEREQUEST_BLOCKSENTRY"]._serialized_options = b"8\001" + _globals["_SUBSCRIBEREQUEST_BLOCKSMETAENTRY"]._loaded_options = None + _globals["_SUBSCRIBEREQUEST_BLOCKSMETAENTRY"]._serialized_options = b"8\001" + _globals["_SUBSCRIBEREQUEST_ENTRYENTRY"]._loaded_options = None + _globals["_SUBSCRIBEREQUEST_ENTRYENTRY"]._serialized_options = b"8\001" + _globals["_COMMITMENTLEVEL"]._serialized_start = 6020 + _globals["_COMMITMENTLEVEL"]._serialized_end = 6151 + _globals["_SUBSCRIBEREQUEST"]._serialized_start = 47 + _globals["_SUBSCRIBEREQUEST"]._serialized_end = 1317 + _globals["_SUBSCRIBEREQUEST_ACCOUNTSENTRY"]._serialized_start = 662 + _globals["_SUBSCRIBEREQUEST_ACCOUNTSENTRY"]._serialized_end = 749 + _globals["_SUBSCRIBEREQUEST_SLOTSENTRY"]._serialized_start = 751 + _globals["_SUBSCRIBEREQUEST_SLOTSENTRY"]._serialized_end = 832 + _globals["_SUBSCRIBEREQUEST_TRANSACTIONSENTRY"]._serialized_start = 834 + _globals["_SUBSCRIBEREQUEST_TRANSACTIONSENTRY"]._serialized_end = 929 + _globals["_SUBSCRIBEREQUEST_TRANSACTIONSSTATUSENTRY"]._serialized_start = 931 + _globals["_SUBSCRIBEREQUEST_TRANSACTIONSSTATUSENTRY"]._serialized_end = 1032 + _globals["_SUBSCRIBEREQUEST_BLOCKSENTRY"]._serialized_start = 1034 + _globals["_SUBSCRIBEREQUEST_BLOCKSENTRY"]._serialized_end = 1117 + _globals["_SUBSCRIBEREQUEST_BLOCKSMETAENTRY"]._serialized_start = 1119 + _globals["_SUBSCRIBEREQUEST_BLOCKSMETAENTRY"]._serialized_end = 1210 + _globals["_SUBSCRIBEREQUEST_ENTRYENTRY"]._serialized_start = 1212 + _globals["_SUBSCRIBEREQUEST_ENTRYENTRY"]._serialized_end = 1293 + _globals["_SUBSCRIBEREQUESTFILTERACCOUNTS"]._serialized_start = 1320 + _globals["_SUBSCRIBEREQUESTFILTERACCOUNTS"]._serialized_end = 1511 + _globals["_SUBSCRIBEREQUESTFILTERACCOUNTSFILTER"]._serialized_start = 1514 + _globals["_SUBSCRIBEREQUESTFILTERACCOUNTSFILTER"]._serialized_end = 1757 + _globals["_SUBSCRIBEREQUESTFILTERACCOUNTSFILTERMEMCMP"]._serialized_start = 1759 + _globals["_SUBSCRIBEREQUESTFILTERACCOUNTSFILTERMEMCMP"]._serialized_end = 1880 + _globals["_SUBSCRIBEREQUESTFILTERACCOUNTSFILTERLAMPORTS"]._serialized_start = 1882 + _globals["_SUBSCRIBEREQUESTFILTERACCOUNTSFILTERLAMPORTS"]._serialized_end = 1991 + _globals["_SUBSCRIBEREQUESTFILTERSLOTS"]._serialized_start = 1993 + _globals["_SUBSCRIBEREQUESTFILTERSLOTS"]._serialized_end = 2082 + _globals["_SUBSCRIBEREQUESTFILTERTRANSACTIONS"]._serialized_start = 2085 + _globals["_SUBSCRIBEREQUESTFILTERTRANSACTIONS"]._serialized_end = 2295 + _globals["_SUBSCRIBEREQUESTFILTERBLOCKS"]._serialized_start = 2298 + _globals["_SUBSCRIBEREQUESTFILTERBLOCKS"]._serialized_end = 2515 + _globals["_SUBSCRIBEREQUESTFILTERBLOCKSMETA"]._serialized_start = 2517 + _globals["_SUBSCRIBEREQUESTFILTERBLOCKSMETA"]._serialized_end = 2551 + _globals["_SUBSCRIBEREQUESTFILTERENTRY"]._serialized_start = 2553 + _globals["_SUBSCRIBEREQUESTFILTERENTRY"]._serialized_end = 2582 + _globals["_SUBSCRIBEREQUESTACCOUNTSDATASLICE"]._serialized_start = 2584 + _globals["_SUBSCRIBEREQUESTACCOUNTSDATASLICE"]._serialized_end = 2651 + _globals["_SUBSCRIBEREQUESTPING"]._serialized_start = 2653 + _globals["_SUBSCRIBEREQUESTPING"]._serialized_end = 2687 + _globals["_SUBSCRIBEUPDATE"]._serialized_start = 2690 + _globals["_SUBSCRIBEUPDATE"]._serialized_end = 3207 + _globals["_SUBSCRIBEUPDATEACCOUNT"]._serialized_start = 3209 + _globals["_SUBSCRIBEUPDATEACCOUNT"]._serialized_end = 3320 + _globals["_SUBSCRIBEUPDATEACCOUNTINFO"]._serialized_start = 3323 + _globals["_SUBSCRIBEUPDATEACCOUNTINFO"]._serialized_end = 3523 + _globals["_SUBSCRIBEUPDATESLOT"]._serialized_start = 3526 + _globals["_SUBSCRIBEUPDATESLOT"]._serialized_end = 3674 + _globals["_SUBSCRIBEUPDATETRANSACTION"]._serialized_start = 3676 + _globals["_SUBSCRIBEUPDATETRANSACTION"]._serialized_end = 3779 + _globals["_SUBSCRIBEUPDATETRANSACTIONINFO"]._serialized_start = 3782 + _globals["_SUBSCRIBEUPDATETRANSACTIONINFO"]._serialized_end = 3998 + _globals["_SUBSCRIBEUPDATETRANSACTIONSTATUS"]._serialized_start = 4001 + _globals["_SUBSCRIBEUPDATETRANSACTIONSTATUS"]._serialized_end = 4162 + _globals["_SUBSCRIBEUPDATEBLOCK"]._serialized_start = 4165 + _globals["_SUBSCRIBEUPDATEBLOCK"]._serialized_end = 4709 + _globals["_SUBSCRIBEUPDATEBLOCKMETA"]._serialized_start = 4712 + _globals["_SUBSCRIBEUPDATEBLOCKMETA"]._serialized_end = 5066 + _globals["_SUBSCRIBEUPDATEENTRY"]._serialized_start = 5069 + _globals["_SUBSCRIBEUPDATEENTRY"]._serialized_end = 5226 + _globals["_SUBSCRIBEUPDATEPING"]._serialized_start = 5228 + _globals["_SUBSCRIBEUPDATEPING"]._serialized_end = 5249 + _globals["_SUBSCRIBEUPDATEPONG"]._serialized_start = 5251 + _globals["_SUBSCRIBEUPDATEPONG"]._serialized_end = 5284 + _globals["_PINGREQUEST"]._serialized_start = 5286 + _globals["_PINGREQUEST"]._serialized_end = 5314 + _globals["_PONGRESPONSE"]._serialized_start = 5316 + _globals["_PONGRESPONSE"]._serialized_end = 5345 + _globals["_GETLATESTBLOCKHASHREQUEST"]._serialized_start = 5347 + _globals["_GETLATESTBLOCKHASHREQUEST"]._serialized_end = 5439 + _globals["_GETLATESTBLOCKHASHRESPONSE"]._serialized_start = 5441 + _globals["_GETLATESTBLOCKHASHRESPONSE"]._serialized_end = 5535 + _globals["_GETBLOCKHEIGHTREQUEST"]._serialized_start = 5537 + _globals["_GETBLOCKHEIGHTREQUEST"]._serialized_end = 5625 + _globals["_GETBLOCKHEIGHTRESPONSE"]._serialized_start = 5627 + _globals["_GETBLOCKHEIGHTRESPONSE"]._serialized_end = 5673 + _globals["_GETSLOTREQUEST"]._serialized_start = 5675 + _globals["_GETSLOTREQUEST"]._serialized_end = 5756 + _globals["_GETSLOTRESPONSE"]._serialized_start = 5758 + _globals["_GETSLOTRESPONSE"]._serialized_end = 5789 + _globals["_GETVERSIONREQUEST"]._serialized_start = 5791 + _globals["_GETVERSIONREQUEST"]._serialized_end = 5810 + _globals["_GETVERSIONRESPONSE"]._serialized_start = 5812 + _globals["_GETVERSIONRESPONSE"]._serialized_end = 5849 + _globals["_ISBLOCKHASHVALIDREQUEST"]._serialized_start = 5851 + _globals["_ISBLOCKHASHVALIDREQUEST"]._serialized_end = 5960 + _globals["_ISBLOCKHASHVALIDRESPONSE"]._serialized_start = 5962 + _globals["_ISBLOCKHASHVALIDRESPONSE"]._serialized_end = 6017 + _globals["_GEYSER"]._serialized_start = 6154 + _globals["_GEYSER"]._serialized_end = 6685 # @@protoc_insertion_point(module_scope) diff --git a/learning-examples/listen-new-tokens/generated/geyser_pb2.pyi b/learning-examples/listen-new-tokens/generated/geyser_pb2.pyi index 909f4fb..8abc78c 100644 --- a/learning-examples/listen-new-tokens/generated/geyser_pb2.pyi +++ b/learning-examples/listen-new-tokens/generated/geyser_pb2.pyi @@ -3,7 +3,13 @@ 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 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 @@ -41,6 +47,7 @@ class CommitmentLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): COMPLETED: _ClassVar[CommitmentLevel] CREATED_BANK: _ClassVar[CommitmentLevel] DEAD: _ClassVar[CommitmentLevel] + PROCESSED: CommitmentLevel CONFIRMED: CommitmentLevel FINALIZED: CommitmentLevel @@ -50,56 +57,106 @@ CREATED_BANK: CommitmentLevel DEAD: CommitmentLevel class SubscribeRequest(_message.Message): - __slots__ = ("accounts", "slots", "transactions", "transactions_status", "blocks", "blocks_meta", "entry", "commitment", "accounts_data_slice", "ping") + __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: ... + 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: ... + 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: ... + 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: ... + 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: ... + 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: ... + 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: ... + 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] @@ -118,9 +175,29 @@ class SubscribeRequest(_message.Message): blocks_meta: _containers.MessageMap[str, SubscribeRequestFilterBlocksMeta] entry: _containers.MessageMap[str, SubscribeRequestFilterEntry] commitment: CommitmentLevel - accounts_data_slice: _containers.RepeatedCompositeFieldContainer[SubscribeRequestAccountsDataSlice] + 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: ... + 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") @@ -130,9 +207,19 @@ class SubscribeRequestFilterAccounts(_message.Message): NONEMPTY_TXN_SIGNATURE_FIELD_NUMBER: _ClassVar[int] account: _containers.RepeatedScalarFieldContainer[str] owner: _containers.RepeatedScalarFieldContainer[str] - filters: _containers.RepeatedCompositeFieldContainer[SubscribeRequestFilterAccountsFilter] + 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: ... + 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") @@ -144,7 +231,17 @@ class SubscribeRequestFilterAccountsFilter(_message.Message): 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: ... + 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") @@ -156,7 +253,13 @@ class SubscribeRequestFilterAccountsFilterMemcmp(_message.Message): bytes: bytes base58: str base64: str - def __init__(self, offset: _Optional[int] = ..., bytes: _Optional[bytes] = ..., base58: _Optional[str] = ..., base64: _Optional[str] = ...) -> None: ... + 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") @@ -168,7 +271,13 @@ class SubscribeRequestFilterAccountsFilterLamports(_message.Message): ne: int lt: int gt: int - def __init__(self, eq: _Optional[int] = ..., ne: _Optional[int] = ..., lt: _Optional[int] = ..., gt: _Optional[int] = ...) -> None: ... + def __init__( + self, + eq: _Optional[int] = ..., + ne: _Optional[int] = ..., + lt: _Optional[int] = ..., + gt: _Optional[int] = ..., + ) -> None: ... class SubscribeRequestFilterSlots(_message.Message): __slots__ = ("filter_by_commitment",) @@ -177,7 +286,14 @@ class SubscribeRequestFilterSlots(_message.Message): def __init__(self, filter_by_commitment: bool = ...) -> None: ... class SubscribeRequestFilterTransactions(_message.Message): - __slots__ = ("vote", "failed", "signature", "account_include", "account_exclude", "account_required") + __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] @@ -190,10 +306,23 @@ class SubscribeRequestFilterTransactions(_message.Message): 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: ... + 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") + __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] @@ -202,7 +331,13 @@ class SubscribeRequestFilterBlocks(_message.Message): 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: ... + def __init__( + self, + account_include: _Optional[_Iterable[str]] = ..., + include_transactions: bool = ..., + include_accounts: bool = ..., + include_entries: bool = ..., + ) -> None: ... class SubscribeRequestFilterBlocksMeta(_message.Message): __slots__ = () @@ -218,7 +353,9 @@ class SubscribeRequestAccountsDataSlice(_message.Message): LENGTH_FIELD_NUMBER: _ClassVar[int] offset: int length: int - def __init__(self, offset: _Optional[int] = ..., length: _Optional[int] = ...) -> None: ... + def __init__( + self, offset: _Optional[int] = ..., length: _Optional[int] = ... + ) -> None: ... class SubscribeRequestPing(_message.Message): __slots__ = ("id",) @@ -227,7 +364,18 @@ class SubscribeRequestPing(_message.Message): def __init__(self, id: _Optional[int] = ...) -> None: ... class SubscribeUpdate(_message.Message): - __slots__ = ("filters", "account", "slot", "transaction", "transaction_status", "block", "ping", "pong", "block_meta", "entry") + __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] @@ -248,7 +396,21 @@ class SubscribeUpdate(_message.Message): 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: ... + 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") @@ -258,10 +420,24 @@ class SubscribeUpdateAccount(_message.Message): account: SubscribeUpdateAccountInfo slot: int is_startup: bool - def __init__(self, account: _Optional[_Union[SubscribeUpdateAccountInfo, _Mapping]] = ..., slot: _Optional[int] = ..., is_startup: bool = ...) -> None: ... + 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") + __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] @@ -278,7 +454,17 @@ class SubscribeUpdateAccountInfo(_message.Message): 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: ... + 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") @@ -290,7 +476,13 @@ class SubscribeUpdateSlot(_message.Message): 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: ... + 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") @@ -298,7 +490,11 @@ class SubscribeUpdateTransaction(_message.Message): SLOT_FIELD_NUMBER: _ClassVar[int] transaction: SubscribeUpdateTransactionInfo slot: int - def __init__(self, transaction: _Optional[_Union[SubscribeUpdateTransactionInfo, _Mapping]] = ..., slot: _Optional[int] = ...) -> None: ... + def __init__( + self, + transaction: _Optional[_Union[SubscribeUpdateTransactionInfo, _Mapping]] = ..., + slot: _Optional[int] = ..., + ) -> None: ... class SubscribeUpdateTransactionInfo(_message.Message): __slots__ = ("signature", "is_vote", "transaction", "meta", "index") @@ -312,7 +508,16 @@ class SubscribeUpdateTransactionInfo(_message.Message): 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: ... + 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") @@ -326,10 +531,31 @@ class SubscribeUpdateTransactionStatus(_message.Message): 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: ... + 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") + __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] @@ -351,15 +577,50 @@ class SubscribeUpdateBlock(_message.Message): parent_slot: int parent_blockhash: str executed_transaction_count: int - transactions: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateTransactionInfo] + 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: ... + 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") + __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] @@ -378,10 +639,32 @@ class SubscribeUpdateBlockMeta(_message.Message): 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: ... + 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") + __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] @@ -394,7 +677,15 @@ class SubscribeUpdateEntry(_message.Message): 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: ... + 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__ = () @@ -422,7 +713,9 @@ class GetLatestBlockhashRequest(_message.Message): __slots__ = ("commitment",) COMMITMENT_FIELD_NUMBER: _ClassVar[int] commitment: CommitmentLevel - def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ... + def __init__( + self, commitment: _Optional[_Union[CommitmentLevel, str]] = ... + ) -> None: ... class GetLatestBlockhashResponse(_message.Message): __slots__ = ("slot", "blockhash", "last_valid_block_height") @@ -432,13 +725,20 @@ class GetLatestBlockhashResponse(_message.Message): 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: ... + 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: ... + def __init__( + self, commitment: _Optional[_Union[CommitmentLevel, str]] = ... + ) -> None: ... class GetBlockHeightResponse(_message.Message): __slots__ = ("block_height",) @@ -450,7 +750,9 @@ class GetSlotRequest(_message.Message): __slots__ = ("commitment",) COMMITMENT_FIELD_NUMBER: _ClassVar[int] commitment: CommitmentLevel - def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ... + def __init__( + self, commitment: _Optional[_Union[CommitmentLevel, str]] = ... + ) -> None: ... class GetSlotResponse(_message.Message): __slots__ = ("slot",) @@ -474,7 +776,11 @@ class IsBlockhashValidRequest(_message.Message): COMMITMENT_FIELD_NUMBER: _ClassVar[int] blockhash: str commitment: CommitmentLevel - def __init__(self, blockhash: _Optional[str] = ..., commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ... + def __init__( + self, + blockhash: _Optional[str] = ..., + commitment: _Optional[_Union[CommitmentLevel, str]] = ..., + ) -> None: ... class IsBlockhashValidResponse(_message.Message): __slots__ = ("slot", "valid") diff --git a/learning-examples/listen-new-tokens/generated/geyser_pb2_grpc.py b/learning-examples/listen-new-tokens/generated/geyser_pb2_grpc.py index f56411e..6c38723 100644 --- a/learning-examples/listen-new-tokens/generated/geyser_pb2_grpc.py +++ b/learning-examples/listen-new-tokens/generated/geyser_pb2_grpc.py @@ -5,23 +5,26 @@ import grpc import generated.geyser_pb2 as geyser__pb2 -GRPC_GENERATED_VERSION = '1.71.0' +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) + + _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}.' + 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}." ) @@ -35,40 +38,47 @@ class GeyserStub: 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) + "/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) + "/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) + "/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) + "/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) + "/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) + "/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) + "/geyser.Geyser/GetVersion", + request_serializer=geyser__pb2.GetVersionRequest.SerializeToString, + response_deserializer=geyser__pb2.GetVersionResponse.FromString, + _registered_method=True, + ) class GeyserServicer: @@ -77,109 +87,112 @@ class GeyserServicer: 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!') + 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!') + 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!') + 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!') + 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!') + 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!') + 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!') + 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, - ), + "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) + "geyser.Geyser", rpc_method_handlers + ) server.add_generic_rpc_handlers((generic_handler,)) - server.add_registered_method_handlers('geyser.Geyser', rpc_method_handlers) + server.add_registered_method_handlers("geyser.Geyser", rpc_method_handlers) - # This class is part of an EXPERIMENTAL API. +# 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): + 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.Geyser/Subscribe", geyser__pb2.SubscribeRequest.SerializeToString, geyser__pb2.SubscribeUpdate.FromString, options, @@ -190,23 +203,26 @@ class Geyser: wait_for_ready, timeout, metadata, - _registered_method=True) + _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): + 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.Geyser/Ping", geyser__pb2.PingRequest.SerializeToString, geyser__pb2.PongResponse.FromString, options, @@ -217,23 +233,26 @@ class Geyser: wait_for_ready, timeout, metadata, - _registered_method=True) + _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): + 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.Geyser/GetLatestBlockhash", geyser__pb2.GetLatestBlockhashRequest.SerializeToString, geyser__pb2.GetLatestBlockhashResponse.FromString, options, @@ -244,23 +263,26 @@ class Geyser: wait_for_ready, timeout, metadata, - _registered_method=True) + _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): + 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.Geyser/GetBlockHeight", geyser__pb2.GetBlockHeightRequest.SerializeToString, geyser__pb2.GetBlockHeightResponse.FromString, options, @@ -271,23 +293,26 @@ class Geyser: wait_for_ready, timeout, metadata, - _registered_method=True) + _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): + 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.Geyser/GetSlot", geyser__pb2.GetSlotRequest.SerializeToString, geyser__pb2.GetSlotResponse.FromString, options, @@ -298,23 +323,26 @@ class Geyser: wait_for_ready, timeout, metadata, - _registered_method=True) + _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): + 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.Geyser/IsBlockhashValid", geyser__pb2.IsBlockhashValidRequest.SerializeToString, geyser__pb2.IsBlockhashValidResponse.FromString, options, @@ -325,23 +353,26 @@ class Geyser: wait_for_ready, timeout, metadata, - _registered_method=True) + _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): + 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.Geyser/GetVersion", geyser__pb2.GetVersionRequest.SerializeToString, geyser__pb2.GetVersionResponse.FromString, options, @@ -352,4 +383,5 @@ class Geyser: wait_for_ready, timeout, metadata, - _registered_method=True) + _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 index 7458363..d6fa180 100644 --- a/learning-examples/listen-new-tokens/generated/solana_storage_pb2.py +++ b/learning-examples/listen-new-tokens/generated/solana_storage_pb2.py @@ -4,72 +4,73 @@ # 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' + _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') +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) +_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 + _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 index 10312a6..69ec1f9 100644 --- a/learning-examples/listen-new-tokens/generated/solana_storage_pb2.pyi +++ b/learning-examples/listen-new-tokens/generated/solana_storage_pb2.pyi @@ -2,7 +2,13 @@ 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 typing import ( + ClassVar as _ClassVar, + Iterable as _Iterable, + Mapping as _Mapping, + Optional as _Optional, + Union as _Union, +) DESCRIPTOR: _descriptor.FileDescriptor @@ -13,6 +19,7 @@ class RewardType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): Rent: _ClassVar[RewardType] Staking: _ClassVar[RewardType] Voting: _ClassVar[RewardType] + Unspecified: RewardType Fee: RewardType Rent: RewardType @@ -20,7 +27,16 @@ Staking: RewardType Voting: RewardType class ConfirmedBlock(_message.Message): - __slots__ = ("previous_blockhash", "blockhash", "parent_slot", "transactions", "rewards", "block_time", "block_height", "num_partitions") + __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] @@ -37,7 +53,19 @@ class ConfirmedBlock(_message.Message): 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: ... + 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") @@ -45,7 +73,11 @@ class ConfirmedTransaction(_message.Message): META_FIELD_NUMBER: _ClassVar[int] transaction: Transaction meta: TransactionStatusMeta - def __init__(self, transaction: _Optional[_Union[Transaction, _Mapping]] = ..., meta: _Optional[_Union[TransactionStatusMeta, _Mapping]] = ...) -> None: ... + def __init__( + self, + transaction: _Optional[_Union[Transaction, _Mapping]] = ..., + meta: _Optional[_Union[TransactionStatusMeta, _Mapping]] = ..., + ) -> None: ... class Transaction(_message.Message): __slots__ = ("signatures", "message") @@ -53,10 +85,21 @@ class Transaction(_message.Message): MESSAGE_FIELD_NUMBER: _ClassVar[int] signatures: _containers.RepeatedScalarFieldContainer[bytes] message: Message - def __init__(self, signatures: _Optional[_Iterable[bytes]] = ..., message: _Optional[_Union[Message, _Mapping]] = ...) -> None: ... + 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") + __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] @@ -68,18 +111,39 @@ class Message(_message.Message): 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: ... + 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") + __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: ... + 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") @@ -89,10 +153,32 @@ class MessageAddressTableLookup(_message.Message): 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: ... + 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") + __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] @@ -125,7 +211,27 @@ class TransactionStatusMeta(_message.Message): 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: ... + 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",) @@ -139,7 +245,11 @@ class InnerInstructions(_message.Message): INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int] index: int instructions: _containers.RepeatedCompositeFieldContainer[InnerInstruction] - def __init__(self, index: _Optional[int] = ..., instructions: _Optional[_Iterable[_Union[InnerInstruction, _Mapping]]] = ...) -> None: ... + 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") @@ -151,7 +261,13 @@ class InnerInstruction(_message.Message): 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: ... + 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") @@ -161,7 +277,12 @@ class CompiledInstruction(_message.Message): program_id_index: int accounts: bytes data: bytes - def __init__(self, program_id_index: _Optional[int] = ..., accounts: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ... + 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") @@ -175,7 +296,14 @@ class TokenBalance(_message.Message): 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: ... + 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") @@ -187,7 +315,13 @@ class UiTokenAmount(_message.Message): 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: ... + 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") @@ -195,7 +329,9 @@ class ReturnData(_message.Message): DATA_FIELD_NUMBER: _ClassVar[int] program_id: bytes data: bytes - def __init__(self, program_id: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ... + def __init__( + self, program_id: _Optional[bytes] = ..., data: _Optional[bytes] = ... + ) -> None: ... class Reward(_message.Message): __slots__ = ("pubkey", "lamports", "post_balance", "reward_type", "commission") @@ -209,7 +345,14 @@ class Reward(_message.Message): 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: ... + 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") @@ -217,7 +360,11 @@ class Rewards(_message.Message): 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: ... + def __init__( + self, + rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., + num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ..., + ) -> None: ... class UnixTimestamp(_message.Message): __slots__ = ("timestamp",) 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 index 1544a78..60daa15 100644 --- a/learning-examples/listen-new-tokens/generated/solana_storage_pb2_grpc.py +++ b/learning-examples/listen-new-tokens/generated/solana_storage_pb2_grpc.py @@ -1,24 +1,28 @@ # 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_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) + + _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}.' + 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_geyser.py b/learning-examples/listen-new-tokens/listen_geyser.py index 086544f..4ad1070 100644 --- a/learning-examples/listen-new-tokens/listen_geyser.py +++ b/learning-examples/listen-new-tokens/listen_geyser.py @@ -37,9 +37,11 @@ async def create_geyser_connection(): ) else: # Default to basic auth auth = grpc.metadata_call_credentials( - lambda _, callback: callback((("authorization", f"Basic {GEYSER_API_TOKEN}"),), None) + lambda _, callback: callback( + (("authorization", f"Basic {GEYSER_API_TOKEN}"),), None + ) ) - + creds = grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth) channel = grpc.aio.secure_channel(GEYSER_ENDPOINT, creds) return geyser_pb2_grpc.GeyserStub(channel) @@ -58,14 +60,14 @@ def decode_create_instruction(ix_data: bytes, keys, accounts) -> 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 @@ -73,21 +75,21 @@ def decode_create_instruction(ix_data: bytes, keys, accounts) -> dict: length = struct.unpack_from(" dict: "rent": get_account_key(6), "user": get_account_key(7), } - + return token_info @@ -122,26 +124,28 @@ async def monitor_pump(): print(f"Starting Pump.fun token monitor using {AUTH_TYPE.upper()} authentication") stub = await create_geyser_connection() request = create_subscription_request() - + async for update in stub.Subscribe(iter([request])): # Skip non-transaction updates if not update.HasField("transaction"): continue - + tx = update.transaction.transaction.transaction msg = getattr(tx, "message", None) if msg is None: continue - + # Check each instruction in the transaction for ix in msg.instructions: if not ix.data.startswith(PUMP_CREATE_PREFIX): continue info = decode_create_instruction(ix.data, msg.account_keys, ix.accounts) - signature = base58.b58encode(bytes(update.transaction.transaction.signature)).decode() + signature = base58.b58encode( + bytes(update.transaction.transaction.signature) + ).decode() print_token_info(info, signature) if __name__ == "__main__": - asyncio.run(monitor_pump()) \ No newline at end of file + asyncio.run(monitor_pump()) diff --git a/learning-examples/listen-new-tokens/listen_logsubscribe_abc.py b/learning-examples/listen-new-tokens/listen_logsubscribe_abc.py index e5662f3..460ccc6 100644 --- a/learning-examples/listen-new-tokens/listen_logsubscribe_abc.py +++ b/learning-examples/listen-new-tokens/listen_logsubscribe_abc.py @@ -22,7 +22,10 @@ load_dotenv() WSS_ENDPOINT = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") PUMP_PROGRAM_ID = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") -ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") +ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string( + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" +) + def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey: """ diff --git a/learning-examples/manual_buy.py b/learning-examples/manual_buy.py index a9b234d..cddaf49 100644 --- a/learning-examples/manual_buy.py +++ b/learning-examples/manual_buy.py @@ -66,7 +66,7 @@ class BondingCurveState: self.__dict__.update(parsed) # Convert raw bytes to Pubkey for creator field - if hasattr(self, 'creator') and isinstance(self.creator, bytes): + if hasattr(self, "creator") and isinstance(self.creator, bytes): self.creator = Pubkey.from_bytes(self.creator) @@ -95,33 +95,25 @@ def calculate_pump_curve_price(curve_state: BondingCurveState) -> float: def _find_creator_vault(creator: Pubkey) -> Pubkey: derived_address, _ = Pubkey.find_program_address( - [ - b"creator-vault", - bytes(creator) - ], + [b"creator-vault", bytes(creator)], PUMP_PROGRAM, - ) + ) return derived_address def _find_global_volume_accumulator() -> Pubkey: derived_address, _ = Pubkey.find_program_address( - [ - b"global_volume_accumulator" - ], + [b"global_volume_accumulator"], PUMP_PROGRAM, - ) + ) return derived_address def _find_user_volume_accumulator(user: Pubkey) -> Pubkey: derived_address, _ = Pubkey.find_program_address( - [ - b"user_volume_accumulator", - bytes(user) - ], + [b"user_volume_accumulator", bytes(user)], PUMP_PROGRAM, - ) + ) return derived_address @@ -174,8 +166,16 @@ async def buy_token( pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False ), AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=True), - AccountMeta(pubkey=_find_user_volume_accumulator(payer.pubkey()), is_signer=False, is_writable=True), + AccountMeta( + pubkey=_find_global_volume_accumulator(), + is_signer=False, + is_writable=True, + ), + AccountMeta( + pubkey=_find_user_volume_accumulator(payer.pubkey()), + is_signer=False, + is_writable=True, + ), ] discriminator = struct.pack(" Pubkey def find_creator_vault(creator: Pubkey) -> Pubkey: derived_address, _ = Pubkey.find_program_address( - [ - b"creator-vault", - bytes(creator) - ], + [b"creator-vault", bytes(creator)], PUMP_PROGRAM, ) return derived_address @@ -161,9 +158,7 @@ async def sell_token( 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=bonding_curve, is_signer=False, is_writable=True), AccountMeta( pubkey=associated_bonding_curve, is_signer=False, @@ -174,12 +169,8 @@ async def sell_token( 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=payer.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=SYSTEM_PROGRAM, is_signer=False, is_writable=False), AccountMeta( pubkey=creator_vault, is_signer=False, @@ -191,9 +182,7 @@ async def sell_token( AccountMeta( pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False ), - AccountMeta( - pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False - ), + AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), ] discriminator = struct.pack(" tuple[Pubkey, int]: """Find the bonding curve PDA for a mint.""" - return Pubkey.find_program_address( - [b"bonding-curve", bytes(mint)], - PUMP_PROGRAM - ) + return Pubkey.find_program_address([b"bonding-curve", bytes(mint)], PUMP_PROGRAM) def find_associated_bonding_curve(mint: Pubkey, bonding_curve: Pubkey) -> Pubkey: @@ -92,10 +107,7 @@ def find_metadata_address(mint: Pubkey) -> Pubkey: def find_creator_vault(creator: Pubkey) -> Pubkey: """Find the creator vault PDA.""" derived_address, _ = Pubkey.find_program_address( - [ - b"creator-vault", - bytes(creator) - ], + [b"creator-vault", bytes(creator)], PUMP_PROGRAM, ) return derived_address @@ -103,22 +115,17 @@ def find_creator_vault(creator: Pubkey) -> Pubkey: def _find_global_volume_accumulator() -> Pubkey: derived_address, _ = Pubkey.find_program_address( - [ - b"global_volume_accumulator" - ], + [b"global_volume_accumulator"], PUMP_PROGRAM, - ) + ) return derived_address def _find_user_volume_accumulator(user: Pubkey) -> Pubkey: derived_address, _ = Pubkey.find_program_address( - [ - b"user_volume_accumulator", - bytes(user) - ], + [b"user_volume_accumulator", bytes(user)], PUMP_PROGRAM, - ) + ) return derived_address @@ -147,28 +154,32 @@ def create_pump_create_instruction( AccountMeta(pubkey=user, 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_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM, is_signer=False, is_writable=False), + AccountMeta( + pubkey=SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_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), ] - + # Encode string as length-prefixed def encode_string(s: str) -> bytes: - encoded = s.encode('utf-8') + encoded = s.encode("utf-8") return struct.pack(" bytes: return bytes(pubkey) data = ( - CREATE_DISCRIMINATOR + - encode_string(name) + - encode_string(symbol) + - encode_string(uri) + - encode_pubkey(creator) + CREATE_DISCRIMINATOR + + encode_string(name) + + encode_string(symbol) + + encode_string(uri) + + encode_pubkey(creator) ) - + return Instruction(PUMP_PROGRAM, data, accounts) @@ -198,16 +209,22 @@ def create_buy_instruction( AccountMeta(pubkey=creator_vault, is_signer=False, is_writable=True), AccountMeta(pubkey=PUMP_EVENT_AUTHORITY, is_signer=False, is_writable=False), AccountMeta(pubkey=PUMP_PROGRAM, is_signer=False, is_writable=False), - AccountMeta(pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=True), - AccountMeta(pubkey=_find_user_volume_accumulator(user), is_signer=False, is_writable=True), + AccountMeta( + pubkey=_find_global_volume_accumulator(), is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=_find_user_volume_accumulator(user), + is_signer=False, + is_writable=True, + ), ] - + data = ( - BUY_DISCRIMINATOR + - struct.pack(" Pubkey: +async def get_market_address_by_base_mint( + client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey +) -> Pubkey: """Find the market address for a given token mint. - + Searches for the AMM pool that contains the specified token mint as its base token by querying program accounts with a filter for the base_mint field. - + 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) - ] - + 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 + 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 binary data stored in the market account into a structured dictionary containing key market information. - + 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 """ @@ -118,20 +131,24 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict: ("pool_base_token_account", "pubkey"), ("pool_quote_token_account", "pubkey"), ("lp_supply", "u64"), - ("coin_creator", "pubkey") + ("coin_creator", "pubkey"), ] for field_name, field_type in fields: if field_type == "pubkey": - value = data[offset:offset + 32] + 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(" dict: return parsed_data + def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey: """Derive the Program Derived Address (PDA) for a coin creator's vault. - + Calculates the deterministic PDA that serves as the vault authority for a specific coin creator in the PUMP AMM protocol. - + Args: coin_creator: Pubkey of the coin creator account - + Returns: Pubkey of the derived coin creator vault authority - + Note: This vault is used to collect creator fees from token transactions """ derived_address, _ = Pubkey.find_program_address( - [ - b"creator_vault", - bytes(coin_creator) - ], + [b"creator_vault", bytes(coin_creator)], PUMP_AMM_PROGRAM_ID, - ) + ) return derived_address + def find_global_volume_accumulator() -> Pubkey: """Derive the Program Derived Address (PDA) for the global volume accumulator. - + Calculates the deterministic PDA that tracks global trading volume across all pools in the PUMP AMM protocol. - + Returns: Pubkey of the derived global volume accumulator account """ derived_address, _ = Pubkey.find_program_address( - [ - b"global_volume_accumulator" - ], + [b"global_volume_accumulator"], PUMP_AMM_PROGRAM_ID, ) return derived_address + def find_user_volume_accumulator(user: Pubkey) -> Pubkey: """Derive the Program Derived Address (PDA) for a user's volume accumulator. - + Calculates the deterministic PDA that tracks trading volume for a specific user in the PUMP AMM protocol. - + Args: user: Pubkey of the user account - + Returns: Pubkey of the derived user volume accumulator account """ derived_address, _ = Pubkey.find_program_address( - [ - b"user_volume_accumulator", - bytes(user) - ], + [b"user_volume_accumulator", bytes(user)], PUMP_AMM_PROGRAM_ID, ) return derived_address -async def calculate_token_pool_price(client: AsyncClient, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey) -> float: + +async def calculate_token_pool_price( + client: AsyncClient, + pool_base_token_account: Pubkey, + pool_quote_token_account: Pubkey, +) -> float: """Calculate the current price of tokens in an AMM pool. - + Fetches the balance of tokens in the pool and calculates the price ratio between the base token and quote token (typically SOL). - + 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 (SOL per token) """ 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) - + 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 -async def buy_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, coin_creator_vault_authority: Pubkey, - coin_creator_vault_ata: Pubkey, sol_amount_to_spend: int, slippage: float = 0.25) -> str | None: + +async def buy_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, + coin_creator_vault_authority: Pubkey, + coin_creator_vault_ata: Pubkey, + sol_amount_to_spend: int, + slippage: float = 0.25, +) -> str | None: """Buy tokens on the PUMP AMM with slippage protection. - + Executes a token purchase on the PUMP AMM protocol, calculating the expected token amount based on the current pool price and applying slippage protection. - + Args: client: Solana RPC client instance pump_fun_amm_market: Address of the AMM market @@ -251,12 +280,14 @@ async def buy_pump_swap(client: AsyncClient, pump_fun_amm_market: Pubkey, payer: coin_creator_vault_ata: Address of the coin creator's associated token account for fees sol_amount_to_spend: Amount of SOL to spend on the purchase (in SOL, not lamports) slippage: Maximum acceptable price slippage, as a decimal (0.25 = 25%) - + Returns: Transaction signature if successful, None otherwise """ # Calculate token price - token_price_sol = await calculate_token_pool_price(client, pool_base_token_account, pool_quote_token_account) + token_price_sol = await calculate_token_pool_price( + client, pool_base_token_account, pool_quote_token_account + ) print(f"Token price in SOL: {token_price_sol:.10f} SOL") # Calculate maximum SOL input with slippage protection @@ -273,98 +304,115 @@ async def buy_pump_swap(client: AsyncClient, pump_fun_amm_market: Pubkey, payer: # Define all accounts needed for the buy 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=False), - 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), - AccountMeta(pubkey=coin_creator_vault_ata, is_signer=False, is_writable=True), - AccountMeta(pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False), - AccountMeta(pubkey=global_volume_accumulator, is_signer=False, is_writable=True), - AccountMeta(pubkey=user_volume_accumulator, is_signer=False, is_writable=True), + AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=False), + AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False), + AccountMeta(pubkey=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), + AccountMeta(pubkey=coin_creator_vault_ata, is_signer=False, is_writable=True), + AccountMeta( + pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=global_volume_accumulator, is_signer=False, is_writable=True + ), + AccountMeta(pubkey=user_volume_accumulator, is_signer=False, is_writable=True), ] - - data = BUY_DISCRIMINATOR + struct.pack(" Pubkey: +async def get_market_address_by_base_mint( + client: AsyncClient, base_mint_address: Pubkey, amm_program_id: Pubkey +) -> 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) - ] - + 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 + 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 """ @@ -108,20 +121,24 @@ async def get_market_data(client: AsyncClient, market_address: Pubkey) -> dict: ("pool_base_token_account", "pubkey"), ("pool_quote_token_account", "pubkey"), ("lp_supply", "u64"), - ("coin_creator", "pubkey") + ("coin_creator", "pubkey"), ] for field_name, field_type in fields: if field_type == "pubkey": - value = data[offset:offset + 32] + 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(" dict: return parsed_data + def find_coin_creator_vault(coin_creator: Pubkey) -> Pubkey: """Derive the Program Derived Address (PDA) for a coin creator's vault. - + Calculates the deterministic PDA that serves as the vault authority for a specific coin creator in the PUMP AMM protocol. - + Args: coin_creator: Pubkey of the coin creator account - + Returns: Pubkey of the derived coin creator vault authority - + Note: This vault is used to collect creator fees from token transactions """ derived_address, _ = Pubkey.find_program_address( - [ - b"creator_vault", - bytes(coin_creator) - ], + [b"creator_vault", bytes(coin_creator)], PUMP_AMM_PROGRAM_ID, - ) + ) return derived_address -async def calculate_token_pool_price(client: AsyncClient, pool_base_token_account: Pubkey, pool_quote_token_account: Pubkey) -> float: + +async def calculate_token_pool_price( + client: AsyncClient, + pool_base_token_account: Pubkey, + pool_quote_token_account: Pubkey, +) -> float: """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) - + 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 """ @@ -201,22 +224,33 @@ def create_ata_idempotent_ix(payer_pubkey: Pubkey) -> Instruction: 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) + 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, coin_creator_vault_authority: Pubkey, - coin_creator_vault_ata: Pubkey, slippage: float = 0.25) -> str | None: + +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, + coin_creator_vault_authority: Pubkey, + coin_creator_vault_ata: 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 @@ -229,20 +263,24 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_market: Pubkey, payer coin_creator_vault_authority: Address of the coin creator's vault authority coin_creator_vault_ata: Address of the coin creator's associated token account for fees 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 = 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) + 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 @@ -256,28 +294,46 @@ async def sell_pump_swap(client: AsyncClient, pump_fun_amm_market: Pubkey, payer # 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=False), - 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), - AccountMeta(pubkey=coin_creator_vault_ata, is_signer=False, is_writable=True), - AccountMeta(pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False), + AccountMeta(pubkey=pump_fun_amm_market, is_signer=False, is_writable=False), + AccountMeta(pubkey=payer.pubkey(), is_signer=True, is_writable=True), + AccountMeta(pubkey=PUMP_SWAP_GLOBAL_CONFIG, is_signer=False, is_writable=False), + AccountMeta(pubkey=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), + AccountMeta(pubkey=coin_creator_vault_ata, is_signer=False, is_writable=True), + AccountMeta( + pubkey=coin_creator_vault_authority, is_signer=False, is_writable=False + ), ] - - data = SELL_DISCRIMINATOR + struct.pack(" None: # Log supported platforms and listeners try: from platforms import platform_factory + supported_platforms = platform_factory.get_supported_platforms() logging.info(f"Supported platforms: {[p.value for p in supported_platforms]}") - + # Log listener compatibility for each platform from config_loader import get_supported_listeners_for_platform + for platform in supported_platforms: listeners = get_supported_listeners_for_platform(platform) logging.info(f"Platform {platform.value} supports listeners: {listeners}") - + except Exception as e: logging.warning(f"Could not load platform information: {e}") @@ -253,4 +266,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/config_loader.py b/src/config_loader.py index 26b37d5..2b9eea3 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -25,14 +25,50 @@ REQUIRED_FIELDS = [ ] CONFIG_VALIDATION_RULES = [ - ("trade.buy_amount", (int, float), 0, float("inf"), "trade.buy_amount must be a positive number"), + ( + "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"), + ( + "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 @@ -66,17 +102,18 @@ def load_bot_config(path: str) -> dict: load_dotenv(env_file, override=True) resolve_env_vars(config) - + # Set default platform if not specified (backward compatibility) if "platform" not in config: config["platform"] = "pump_fun" - + validate_config(config) return config def resolve_env_vars(config: dict) -> None: """Recursively resolve environment variables in the configuration.""" + def resolve_env(value): if isinstance(value, str) and value.startswith("${") and value.endswith("}"): env_var = value[2:-1] @@ -144,7 +181,9 @@ def validate_config(config: dict) -> None: 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") + raise ValueError( + "Cannot enable both dynamic and fixed priority fees simultaneously" + ) except ValueError as e: if "Missing required config key" not in str(e): raise @@ -156,7 +195,9 @@ def validate_config(config: dict) -> None: validate_platform_config(config, platform) except ValueError as e: if "is not a valid" in str(e): - raise ValueError(f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}") + raise ValueError( + f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}" + ) raise @@ -165,8 +206,11 @@ def validate_platform_config(config: dict, platform: Platform) -> None: # Check if platform is supported try: from platforms import platform_factory + if not platform_factory.registry.is_platform_supported(platform): - raise ValueError(f"Platform {platform.value} is not supported. Available platforms: {[p.value for p in platform_factory.get_supported_platforms()]}") + raise ValueError( + f"Platform {platform.value} is not supported. Available platforms: {[p.value for p in platform_factory.get_supported_platforms()]}" + ) except ImportError: # If platform factory not available, just validate enum pass @@ -175,7 +219,7 @@ def validate_platform_config(config: dict, platform: Platform) -> None: try: listener_type = get_nested_value(config, "filters.listener_type") compatible_listeners = PLATFORM_LISTENER_COMPATIBILITY.get(platform, []) - + if listener_type not in compatible_listeners: raise ValueError( f"Listener type '{listener_type}' is not compatible with platform '{platform.value}'. " @@ -189,7 +233,7 @@ def validate_platform_config(config: dict, platform: Platform) -> None: if platform == Platform.PUMP_FUN: # pump.fun doesn't require additional config beyond base requirements pass - + elif platform == Platform.LETS_BONK: # LetsBonk may require additional configuration in the future # For now, it uses the same base configuration as pump.fun @@ -202,16 +246,20 @@ def get_platform_from_config(config: dict) -> Platform: try: return Platform(platform_str) except ValueError: - raise ValueError(f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}") + raise ValueError( + f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}" + ) -def validate_platform_listener_combination(platform: Platform, listener_type: str) -> bool: +def validate_platform_listener_combination( + platform: Platform, listener_type: str +) -> bool: """Check if a platform and listener type are compatible. - + Args: platform: Platform enum listener_type: Listener type string - + Returns: True if combination is valid """ @@ -221,10 +269,10 @@ def validate_platform_listener_combination(platform: Platform, listener_type: st def get_supported_listeners_for_platform(platform: Platform) -> list[str]: """Get list of supported listener types for a platform. - + Args: platform: Platform enum - + Returns: List of supported listener types """ @@ -233,10 +281,10 @@ def get_supported_listeners_for_platform(platform: Platform) -> list[str]: def get_platform_specific_required_config(platform: Platform) -> list[str]: """Get platform-specific required configuration paths. - + Args: platform: Platform enum - + Returns: List of additional required config paths for the platform """ @@ -251,17 +299,23 @@ def get_platform_specific_required_config(platform: Platform) -> list[str]: def print_config_summary(config: dict) -> None: """Print a summary of the loaded configuration with platform info.""" platform_str = config.get("platform", "pump_fun") - + print(f"Bot name: {config.get('name', 'unnamed')}") print(f"Platform: {platform_str}") - print(f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}") + print( + f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}" + ) # Validate platform-listener combination try: platform = Platform(platform_str) - listener_type = config.get('filters', {}).get('listener_type') - if listener_type and not validate_platform_listener_combination(platform, listener_type): - print(f"WARNING: Listener '{listener_type}' may not be compatible with platform '{platform_str}'") + listener_type = config.get("filters", {}).get("listener_type") + if listener_type and not validate_platform_listener_combination( + platform, listener_type + ): + print( + f"WARNING: Listener '{listener_type}' may not be compatible with platform '{platform_str}'" + ) except ValueError: print(f"WARNING: Invalid platform '{platform_str}'") @@ -269,24 +323,28 @@ def print_config_summary(config: dict) -> None: 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'}") + 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( + f" - Fixed fee: {fees.get('fixed_amount', 'not configured')} microlamports" + ) print("Configuration loaded successfully!") def validate_all_platform_configs(config_dir: str = "bots") -> dict[str, Any]: """Validate all bot configurations in a directory. - + Args: config_dir: Directory containing bot config files - + Returns: Dictionary with validation results """ @@ -296,50 +354,55 @@ def validate_all_platform_configs(config_dir: str = "bots") -> dict[str, Any]: "platform_distribution": {}, "listener_distribution": {}, } - + config_files = list(Path(config_dir).glob("*.yaml")) - + for config_file in config_files: try: config = load_bot_config(config_file) platform = get_platform_from_config(config) - listener_type = config.get('filters', {}).get('listener_type', 'unknown') - - results["valid_configs"].append({ - "file": config_file, - "name": config.get("name"), - "platform": platform.value, - "listener": listener_type, - "enabled": config.get("enabled", True) - }) - + listener_type = config.get("filters", {}).get("listener_type", "unknown") + + results["valid_configs"].append( + { + "file": config_file, + "name": config.get("name"), + "platform": platform.value, + "listener": listener_type, + "enabled": config.get("enabled", True), + } + ) + # Track distributions platform_key = platform.value - results["platform_distribution"][platform_key] = results["platform_distribution"].get(platform_key, 0) + 1 - results["listener_distribution"][listener_type] = results["listener_distribution"].get(listener_type, 0) + 1 - + results["platform_distribution"][platform_key] = ( + results["platform_distribution"].get(platform_key, 0) + 1 + ) + results["listener_distribution"][listener_type] = ( + results["listener_distribution"].get(listener_type, 0) + 1 + ) + except Exception as e: - results["invalid_configs"].append({ - "file": config_file, - "error": str(e) - }) - + results["invalid_configs"].append({"file": config_file, "error": str(e)}) + return results if __name__ == "__main__": # Example usage with platform configuration validation import sys - + if len(sys.argv) > 1: config_path = sys.argv[1] try: config = load_bot_config(config_path) print_config_summary(config) - + platform = get_platform_from_config(config) print(f"Detected platform: {platform}") - print(f"Supported listeners for this platform: {get_supported_listeners_for_platform(platform)}") + print( + f"Supported listeners for this platform: {get_supported_listeners_for_platform(platform)}" + ) except Exception as e: print(f"Configuration error: {e}") else: @@ -350,8 +413,8 @@ if __name__ == "__main__": print(f"Invalid configs: {len(results['invalid_configs'])}") print(f"Platform distribution: {results['platform_distribution']}") print(f"Listener distribution: {results['listener_distribution']}") - - if results['invalid_configs']: + + if results["invalid_configs"]: print("\nInvalid configurations:") - for invalid in results['invalid_configs']: - print(f" {invalid['file']}: {invalid['error']}") \ No newline at end of file + for invalid in results["invalid_configs"]: + print(f" {invalid['file']}: {invalid['error']}") diff --git a/src/core/pubkeys.py b/src/core/pubkeys.py index 586975d..c6a8697 100644 --- a/src/core/pubkeys.py +++ b/src/core/pubkeys.py @@ -14,7 +14,9 @@ TOKEN_DECIMALS: Final[int] = 6 # Token account constants TOKEN_ACCOUNT_SIZE: Final[int] = 165 # Size of a token account in bytes -TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE: Final[int] = 2_039_280 # Rent-exempt minimum for token accounts +TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE: Final[int] = ( + 2_039_280 # Rent-exempt minimum for token accounts +) # Core system programs SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111") @@ -26,9 +28,7 @@ ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( ) # System accounts -RENT: Final[Pubkey] = Pubkey.from_string( - "SysvarRent111111111111111111111111111111111" -) +RENT: Final[Pubkey] = Pubkey.from_string("SysvarRent111111111111111111111111111111111") # Native SOL token SOL_MINT: Final[Pubkey] = Pubkey.from_string( @@ -38,18 +38,18 @@ SOL_MINT: Final[Pubkey] = Pubkey.from_string( class SystemAddresses: """System-level Solana addresses shared across all platforms.""" - + # Reference the module-level constants SYSTEM_PROGRAM = SYSTEM_PROGRAM TOKEN_PROGRAM = TOKEN_PROGRAM ASSOCIATED_TOKEN_PROGRAM = ASSOCIATED_TOKEN_PROGRAM RENT = RENT SOL_MINT = SOL_MINT - + @classmethod def get_all_system_addresses(cls) -> dict[str, Pubkey]: """Get all system addresses as a dictionary. - + Returns: Dictionary mapping address names to Pubkey objects """ @@ -59,4 +59,4 @@ class SystemAddresses: "associated_token_program": cls.ASSOCIATED_TOKEN_PROGRAM, "rent": cls.RENT, "sol_mint": cls.SOL_MINT, - } \ No newline at end of file + } diff --git a/src/interfaces/core.py b/src/interfaces/core.py index 2de0348..d0f9dea 100644 --- a/src/interfaces/core.py +++ b/src/interfaces/core.py @@ -16,6 +16,7 @@ from solders.pubkey import Pubkey class Platform(Enum): """Supported trading platforms.""" + PUMP_FUN = "pump_fun" LETS_BONK = "lets_bonk" @@ -23,12 +24,13 @@ class Platform(Enum): @dataclass class TokenInfo: """Enhanced token information with platform support.""" + # Core token data name: str symbol: str uri: str mint: Pubkey - + # Platform-specific fields platform: Platform bonding_curve: Pubkey | None = None # pump.fun specific @@ -36,12 +38,12 @@ class TokenInfo: pool_state: Pubkey | None = None # LetsBonk specific base_vault: Pubkey | None = None # LetsBonk specific quote_vault: Pubkey | None = None # LetsBonk specific - + # Common fields user: Pubkey | None = None creator: Pubkey | None = None creator_vault: Pubkey | None = None - + # Metadata creation_timestamp: float | None = None additional_data: dict[str, Any] | None = None @@ -49,61 +51,63 @@ class TokenInfo: class AddressProvider(ABC): """Abstract interface for platform-specific address management.""" - + @property @abstractmethod def platform(self) -> Platform: """Get the platform this provider serves.""" pass - + @property @abstractmethod def program_id(self) -> Pubkey: """Get the main program ID for this platform.""" pass - + @abstractmethod def get_system_addresses(self) -> dict[str, Pubkey]: """Get all system addresses required for this platform. - + Returns: Dictionary mapping address names to Pubkey objects """ pass - + @abstractmethod - def derive_pool_address(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey: + def derive_pool_address( + self, base_mint: Pubkey, quote_mint: Pubkey | None = None + ) -> Pubkey: """Derive the pool/curve address for trading pair. - + Args: base_mint: Base token mint address quote_mint: Quote token mint address (if applicable) - + Returns: Pool/curve address for the trading pair """ pass - + @abstractmethod def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey: """Derive user's token account address. - + Args: user: User's wallet address mint: Token mint address - + Returns: User's token account address """ pass - + @abstractmethod def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]: """Get platform-specific additional accounts needed for trading. - + Args: token_info: Token information - + Returns: Dictionary of additional account addresses """ @@ -112,13 +116,13 @@ class AddressProvider(ABC): class InstructionBuilder(ABC): """Abstract interface for building platform-specific trading instructions.""" - + @property @abstractmethod def platform(self) -> Platform: """Get the platform this builder serves.""" pass - + @abstractmethod async def build_buy_instruction( self, @@ -126,22 +130,22 @@ class InstructionBuilder(ABC): user: Pubkey, amount_in: int, minimum_amount_out: int, - address_provider: AddressProvider + address_provider: AddressProvider, ) -> list[Instruction]: """Build buy instruction(s) for the platform. - + Args: token_info: Token information user: User's wallet address amount_in: Amount of quote tokens to spend minimum_amount_out: Minimum base tokens expected address_provider: Platform address provider - + Returns: List of instructions needed for the buy operation """ pass - + @abstractmethod async def build_sell_instruction( self, @@ -149,55 +153,49 @@ class InstructionBuilder(ABC): user: Pubkey, amount_in: int, minimum_amount_out: int, - address_provider: AddressProvider + address_provider: AddressProvider, ) -> list[Instruction]: """Build sell instruction(s) for the platform. - + Args: token_info: Token information user: User's wallet address amount_in: Amount of base tokens to sell minimum_amount_out: Minimum quote tokens expected address_provider: Platform address provider - + Returns: List of instructions needed for the sell operation """ pass - + @abstractmethod def get_required_accounts_for_buy( - self, - token_info: TokenInfo, - user: Pubkey, - address_provider: AddressProvider + self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider ) -> list[Pubkey]: """Get list of accounts required for buy operation (for priority fee calculation). - + Args: token_info: Token information user: User's wallet address address_provider: Platform address provider - + Returns: List of account addresses that will be accessed """ pass - + @abstractmethod def get_required_accounts_for_sell( - self, - token_info: TokenInfo, - user: Pubkey, - address_provider: AddressProvider + self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider ) -> list[Pubkey]: """Get list of accounts required for sell operation (for priority fee calculation). - + Args: token_info: Token information user: User's wallet address address_provider: Platform address provider - + Returns: List of account addresses that will be accessed """ @@ -206,78 +204,74 @@ class InstructionBuilder(ABC): class CurveManager(ABC): """Abstract interface for platform-specific price calculations and pool state management.""" - + @property @abstractmethod def platform(self) -> Platform: """Get the platform this manager serves.""" pass - + @abstractmethod async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]: """Get the current state of a trading pool/curve. - + Args: pool_address: Address of the pool/curve - + Returns: Dictionary containing pool state data """ pass - + @abstractmethod async def calculate_price(self, pool_address: Pubkey) -> float: """Calculate current token price from pool state. - + Args: pool_address: Address of the pool/curve - + Returns: Current token price in quote token units """ pass - + @abstractmethod async def calculate_buy_amount_out( - self, - pool_address: Pubkey, - amount_in: int + self, pool_address: Pubkey, amount_in: int ) -> int: """Calculate expected tokens received for a buy operation. - + Args: pool_address: Address of the pool/curve amount_in: Amount of quote tokens to spend - + Returns: Expected amount of base tokens to receive """ pass - + @abstractmethod async def calculate_sell_amount_out( - self, - pool_address: Pubkey, - amount_in: int + self, pool_address: Pubkey, amount_in: int ) -> int: """Calculate expected quote tokens received for a sell operation. - + Args: pool_address: Address of the pool/curve amount_in: Amount of base tokens to sell - + Returns: Expected amount of quote tokens to receive """ pass - + @abstractmethod async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]: """Get current pool reserves. - + Args: pool_address: Address of the pool/curve - + Returns: Tuple of (base_reserves, quote_reserves) """ @@ -286,93 +280,86 @@ class CurveManager(ABC): class EventParser(ABC): """Abstract interface for parsing platform-specific token creation events.""" - + @property @abstractmethod def platform(self) -> Platform: """Get the platform this parser serves.""" pass - + @abstractmethod def parse_token_creation_from_logs( - self, - logs: list[str], - signature: str + self, logs: list[str], signature: str ) -> TokenInfo | None: """Parse token creation from transaction logs. - + Args: logs: List of log strings from transaction signature: Transaction signature - + Returns: TokenInfo if token creation found, None otherwise """ pass - + @abstractmethod def parse_token_creation_from_instruction( - self, - instruction_data: bytes, - accounts: list[int], - account_keys: list[bytes] + self, instruction_data: bytes, accounts: list[int], account_keys: list[bytes] ) -> TokenInfo | None: """Parse token creation from instruction data. - + Args: instruction_data: Raw instruction data accounts: List of account indices account_keys: List of account public keys - + Returns: TokenInfo if token creation found, None otherwise """ pass - + @abstractmethod def parse_token_creation_from_geyser( - self, - transaction_info: Any + self, transaction_info: Any ) -> TokenInfo | None: """Parse token creation from Geyser transaction data. - + Args: transaction_info: Geyser transaction information - + Returns: TokenInfo if token creation found, None otherwise """ pass - + @abstractmethod def parse_token_creation_from_block( - self, - block_data: dict[str, Any] + self, block_data: dict[str, Any] ) -> TokenInfo | None: """Parse token creation from block data. - + Args: block_data: Block data containing transactions - + Returns: TokenInfo if token creation found, None otherwise """ pass - + @abstractmethod def get_program_id(self) -> Pubkey: """Get the program ID this parser monitors. - + Returns: Program ID for event filtering """ pass - + @abstractmethod def get_instruction_discriminators(self) -> list[bytes]: """Get instruction discriminators for token creation. - + Returns: List of discriminator bytes to match """ - pass \ No newline at end of file + pass diff --git a/src/monitoring/base_listener.py b/src/monitoring/base_listener.py index 10184e2..ed3d283 100644 --- a/src/monitoring/base_listener.py +++ b/src/monitoring/base_listener.py @@ -13,7 +13,7 @@ class BaseTokenListener(ABC): def __init__(self, platform: Platform | None = None): """Initialize the listener with optional platform specification. - + Args: platform: Platform to monitor (if None, monitor all platforms) """ @@ -38,13 +38,13 @@ class BaseTokenListener(ABC): def should_process_token(self, token_info: TokenInfo) -> bool: """Check if a token should be processed based on platform filter. - + Args: token_info: Token information - + Returns: True if token should be processed """ if self.platform is None: return True # Process all platforms - return token_info.platform == self.platform \ No newline at end of file + return token_info.platform == self.platform diff --git a/src/monitoring/listener_factory.py b/src/monitoring/listener_factory.py index cb3110d..5144bb4 100644 --- a/src/monitoring/listener_factory.py +++ b/src/monitoring/listener_factory.py @@ -48,7 +48,7 @@ class ListenerFactory: ) from monitoring.universal_geyser_listener import UniversalGeyserListener - + listener = UniversalGeyserListener( geyser_endpoint=geyser_endpoint, geyser_api_token=geyser_api_token, @@ -63,7 +63,7 @@ class ListenerFactory: raise ValueError("WebSocket endpoint is required for logs listener") from monitoring.universal_logs_listener import UniversalLogsListener - + listener = UniversalLogsListener( wss_endpoint=wss_endpoint, platforms=platforms, @@ -76,7 +76,7 @@ class ListenerFactory: raise ValueError("WebSocket endpoint is required for blocks listener") from monitoring.universal_block_listener import UniversalBlockListener - + listener = UniversalBlockListener( wss_endpoint=wss_endpoint, platforms=platforms, @@ -89,26 +89,36 @@ class ListenerFactory: from monitoring.universal_pumpportal_listener import ( UniversalPumpPortalListener, ) - + # Validate that requested platforms support PumpPortal supported_pumpportal_platforms = [Platform.PUMP_FUN, Platform.LETS_BONK] - + if platforms: - unsupported = [p for p in platforms if p not in supported_pumpportal_platforms] + unsupported = [ + p for p in platforms if p not in supported_pumpportal_platforms + ] if unsupported: - logger.warning(f"Platforms {[p.value for p in unsupported]} do not support PumpPortal") - + logger.warning( + f"Platforms {[p.value for p in unsupported]} do not support PumpPortal" + ) + # Filter to only supported platforms - filtered_platforms = [p for p in platforms if p in supported_pumpportal_platforms] + filtered_platforms = [ + p for p in platforms if p in supported_pumpportal_platforms + ] if not filtered_platforms: - raise ValueError("No supported platforms specified for PumpPortal listener") + raise ValueError( + "No supported platforms specified for PumpPortal listener" + ) platforms = filtered_platforms - + listener = UniversalPumpPortalListener( pumpportal_url=pumpportal_url, platforms=platforms, ) - logger.info(f"Created Universal PumpPortal listener for platforms: {[p.value for p in (platforms or supported_pumpportal_platforms)]}") + logger.info( + f"Created Universal PumpPortal listener for platforms: {[p.value for p in (platforms or supported_pumpportal_platforms)]}" + ) return listener else: @@ -146,8 +156,8 @@ class ListenerFactory: @staticmethod def get_pumpportal_supported_platforms() -> list[Platform]: """Get list of platforms that support PumpPortal listener. - + Returns: List of platforms with PumpPortal support """ - return [Platform.PUMP_FUN, Platform.LETS_BONK] \ No newline at end of file + return [Platform.PUMP_FUN, Platform.LETS_BONK] diff --git a/src/monitoring/universal_block_listener.py b/src/monitoring/universal_block_listener.py index 1daa42e..be90396 100644 --- a/src/monitoring/universal_block_listener.py +++ b/src/monitoring/universal_block_listener.py @@ -33,25 +33,25 @@ class UniversalBlockListener(BaseTokenListener): super().__init__() self.wss_endpoint = wss_endpoint self.ping_interval = 20 # seconds - + # Import platform factory and get supported platforms from platforms import platform_factory - + if platforms is None: # Monitor all supported platforms self.platforms = platform_factory.get_supported_platforms() else: self.platforms = platforms - + # Get event parsers for all platforms self.platform_parsers = {} self.platform_program_ids = [] - + for platform in self.platforms: try: # Create a simple dummy client that doesn't start blockhash updater from core.client import SolanaClient - + # Create a mock client class to avoid network operations class DummyClient(SolanaClient): def __init__(self): @@ -61,16 +61,18 @@ class UniversalBlockListener(BaseTokenListener): self._cached_blockhash = None self._blockhash_lock = None self._blockhash_updater_task = None - + dummy_client = DummyClient() - + implementations = get_platform_implementations(platform, dummy_client) parser = implementations.event_parser self.platform_parsers[platform] = parser self.platform_program_ids.append(str(parser.get_program_id())) - - logger.info(f"Registered platform {platform.value} with program ID {parser.get_program_id()}") - + + logger.info( + f"Registered platform {platform.value} with program ID {parser.get_program_id()}" + ) + except Exception as e: logger.warning(f"Could not register platform {platform.value}: {e}") @@ -117,7 +119,10 @@ class UniversalBlockListener(BaseTokenListener): ) continue - if creator_address and str(token_info.user) != creator_address: + if ( + creator_address + and str(token_info.user) != creator_address + ): logger.info( f"Token not created by {creator_address}. Skipping..." ) @@ -220,10 +225,10 @@ class UniversalBlockListener(BaseTokenListener): for platform, parser in self.platform_parsers.items(): # Check if the parser has a block parsing method - if hasattr(parser, 'parse_token_creation_from_block'): - token_info = parser.parse_token_creation_from_block({ - "transactions": [tx] - }) + if hasattr(parser, "parse_token_creation_from_block"): + token_info = parser.parse_token_creation_from_block( + {"transactions": [tx]} + ) if token_info: return token_info @@ -237,4 +242,4 @@ class UniversalBlockListener(BaseTokenListener): except Exception: logger.exception("Error processing WebSocket message") - return None \ No newline at end of file + return None diff --git a/src/monitoring/universal_geyser_listener.py b/src/monitoring/universal_geyser_listener.py index cf2933d..0bdd2ec 100644 --- a/src/monitoring/universal_geyser_listener.py +++ b/src/monitoring/universal_geyser_listener.py @@ -30,7 +30,7 @@ class UniversalGeyserListener(BaseTokenListener): super().__init__() self.geyser_endpoint = geyser_endpoint self.geyser_api_token = geyser_api_token - + valid_auth_types = {"x-token", "basic"} self.auth_type: str = (geyser_auth_type or "x-token").lower() if self.auth_type not in valid_auth_types: @@ -38,21 +38,21 @@ class UniversalGeyserListener(BaseTokenListener): f"Unsupported auth_type={self.auth_type!r}. " f"Expected one of {valid_auth_types}" ) - + if platforms is None: self.platforms = platform_factory.get_supported_platforms() else: self.platforms = platforms - + # Get event parsers for all platforms self.platform_parsers = {} self.platform_program_ids = set() - + for platform in self.platforms: try: # Create a simple dummy client that doesn't start blockhash updater from core.client import SolanaClient - + # Create a mock client class to avoid network operations class DummyClient(SolanaClient): def __init__(self): @@ -62,22 +62,26 @@ class UniversalGeyserListener(BaseTokenListener): self._cached_blockhash = None self._blockhash_lock = None self._blockhash_updater_task = None - + dummy_client = DummyClient() - - implementations = platform_factory.create_for_platform(platform, dummy_client) + + implementations = platform_factory.create_for_platform( + platform, dummy_client + ) parser = implementations.event_parser self.platform_parsers[platform] = parser self.platform_program_ids.add(parser.get_program_id()) - - logger.info(f"Registered platform {platform.value} with program ID {parser.get_program_id()}") - + + logger.info( + f"Registered platform {platform.value} with program ID {parser.get_program_id()}" + ) + except Exception as e: logger.warning(f"Could not register platform {platform.value}: {e}") async def _create_geyser_connection(self): """Establish a secure connection to the Geyser endpoint.""" - + if self.auth_type == "x-token": auth = grpc.metadata_call_credentials( lambda _, callback: callback( @@ -92,20 +96,20 @@ class UniversalGeyserListener(BaseTokenListener): ) 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 all monitored platforms.""" - + request = geyser_pb2.SubscribeRequest() - + # Add all platform program IDs to the filter for program_id in self.platform_program_ids: filter_name = f"platform_filter_{program_id}" request.transactions[filter_name].account_include.append(str(program_id)) request.transactions[filter_name].failed = False - + request.commitment = geyser_pb2.CommitmentLevel.PROCESSED return request @@ -126,8 +130,12 @@ class UniversalGeyserListener(BaseTokenListener): request = self._create_subscription_request() logger.info(f"Connected to Geyser endpoint: {self.geyser_endpoint}") - logger.info(f"Monitoring platforms: {[p.value for p in self.platforms]}") - logger.info(f"Monitoring program IDs: {[str(pid) for pid in self.platform_program_ids]}") + logger.info( + f"Monitoring platforms: {[p.value for p in self.platforms]}" + ) + logger.info( + f"Monitoring program IDs: {[str(pid) for pid in self.platform_program_ids]}" + ) try: async for update in stub.Subscribe(iter([request])): @@ -192,7 +200,7 @@ class UniversalGeyserListener(BaseTokenListener): continue program_id = Pubkey.from_bytes(msg.account_keys[program_idx]) - + # Find the matching platform parser for platform, parser in self.platform_parsers.items(): if program_id == parser.get_program_id(): @@ -207,4 +215,4 @@ class UniversalGeyserListener(BaseTokenListener): except Exception: logger.exception("Error processing Geyser update") - return None \ No newline at end of file + return None diff --git a/src/monitoring/universal_logs_listener.py b/src/monitoring/universal_logs_listener.py index b6fe49c..325916e 100644 --- a/src/monitoring/universal_logs_listener.py +++ b/src/monitoring/universal_logs_listener.py @@ -1,6 +1,7 @@ """ Universal logs listener that works with any platform through the interface system. """ + import asyncio import json from collections.abc import Awaitable, Callable @@ -31,25 +32,25 @@ class UniversalLogsListener(BaseTokenListener): super().__init__() self.wss_endpoint = wss_endpoint self.ping_interval = 20 # seconds - + # Import platform factory and get supported platforms from platforms import platform_factory - + if platforms is None: # Monitor all supported platforms self.platforms = platform_factory.get_supported_platforms() else: self.platforms = platforms - + # Get event parsers for all platforms self.platform_parsers = {} self.platform_program_ids = [] - + for platform in self.platforms: try: # Create a simple dummy client that doesn't start blockhash updater from core.client import SolanaClient - + # Create a mock client class to avoid network operations class DummyClient(SolanaClient): def __init__(self): @@ -59,16 +60,20 @@ class UniversalLogsListener(BaseTokenListener): self._cached_blockhash = None self._blockhash_lock = None self._blockhash_updater_task = None - + dummy_client = DummyClient() - - implementations = platform_factory.create_for_platform(platform, dummy_client) + + implementations = platform_factory.create_for_platform( + platform, dummy_client + ) parser = implementations.event_parser self.platform_parsers[platform] = parser self.platform_program_ids.append(str(parser.get_program_id())) - - logger.info(f"Registered platform {platform.value} with program ID {parser.get_program_id()}") - + + logger.info( + f"Registered platform {platform.value} with program ID {parser.get_program_id()}" + ) + except Exception as e: logger.warning(f"Could not register platform {platform.value}: {e}") @@ -115,7 +120,10 @@ class UniversalLogsListener(BaseTokenListener): ) continue - if creator_address and str(token_info.user) != creator_address: + if ( + creator_address + and str(token_info.user) != creator_address + ): logger.info( f"Token not created by {creator_address}. Skipping..." ) @@ -159,7 +167,9 @@ class UniversalLogsListener(BaseTokenListener): response = await websocket.recv() response_data = json.loads(response) if "result" in response_data: - logger.info(f"Subscription confirmed with ID: {response_data['result']}") + logger.info( + f"Subscription confirmed with ID: {response_data['result']}" + ) else: logger.warning(f"Unexpected subscription response: {response}") @@ -209,4 +219,4 @@ class UniversalLogsListener(BaseTokenListener): except Exception: logger.exception("Error processing WebSocket message") - return None \ No newline at end of file + return None diff --git a/src/monitoring/universal_pumpportal_listener.py b/src/monitoring/universal_pumpportal_listener.py index 5a4818e..67aea0e 100644 --- a/src/monitoring/universal_pumpportal_listener.py +++ b/src/monitoring/universal_pumpportal_listener.py @@ -32,23 +32,23 @@ class UniversalPumpPortalListener(BaseTokenListener): super().__init__() self.pumpportal_url = pumpportal_url self.ping_interval = 20 # seconds - + # Get platform-specific processors from platforms.letsbonk.pumpportal_processor import LetsBonkPumpPortalProcessor from platforms.pumpfun.pumpportal_processor import PumpFunPumpPortalProcessor - + # Create processor instances all_processors = [ PumpFunPumpPortalProcessor(), LetsBonkPumpPortalProcessor(), ] - + # Filter processors based on requested platforms if platforms is None: self.processors = all_processors else: self.processors = [p for p in all_processors if p.platform in platforms] - + # Build mapping of pool names to processors for quick lookup self.pool_to_processors: dict[str, list] = {} for processor in self.processors: @@ -56,8 +56,10 @@ class UniversalPumpPortalListener(BaseTokenListener): if pool_name not in self.pool_to_processors: self.pool_to_processors[pool_name] = [] self.pool_to_processors[pool_name].append(processor) - - logger.info(f"Initialized Universal PumpPortal listener for platforms: {[p.platform.value for p in self.processors]}") + + logger.info( + f"Initialized Universal PumpPortal listener for platforms: {[p.platform.value for p in self.processors]}" + ) logger.info(f"Monitoring pools: {list(self.pool_to_processors.keys())}") async def listen_for_tokens( @@ -100,8 +102,14 @@ class UniversalPumpPortalListener(BaseTokenListener): continue if creator_address: - creator_str = str(token_info.creator) if token_info.creator else "" - user_str = str(token_info.user) if token_info.user else "" + creator_str = ( + str(token_info.creator) + if token_info.creator + else "" + ) + user_str = ( + str(token_info.user) if token_info.user else "" + ) if creator_address not in [creator_str, user_str]: logger.info( f"Token not created by {creator_address}. Skipping..." @@ -197,7 +205,9 @@ class UniversalPumpPortalListener(BaseTokenListener): if processor.can_process(token_data): token_info = processor.process_token_data(token_data) if token_info: - logger.debug(f"Successfully processed token using {processor.platform.value} processor") + logger.debug( + f"Successfully processed token using {processor.platform.value} processor" + ) return token_info logger.debug(f"No processor could handle token data from pool {pool_name}") @@ -213,4 +223,4 @@ class UniversalPumpPortalListener(BaseTokenListener): except Exception: logger.exception("Error processing PumpPortal WebSocket message") - return None \ No newline at end of file + return None diff --git a/src/platforms/__init__.py b/src/platforms/__init__.py index f7dd9b5..230c107 100644 --- a/src/platforms/__init__.py +++ b/src/platforms/__init__.py @@ -25,6 +25,7 @@ logger = get_logger(__name__) @dataclass class PlatformImplementations: """Container for all platform-specific implementations.""" + address_provider: AddressProvider instruction_builder: InstructionBuilder curve_manager: CurveManager @@ -33,21 +34,21 @@ class PlatformImplementations: class PlatformRegistry: """Registry for platform implementations.""" - + def __init__(self): self._implementations: dict[Platform, dict[str, type]] = {} self._instances: dict[tuple[Platform, str], PlatformImplementations] = {} - + def register_platform( self, platform: Platform, address_provider_class: type[AddressProvider], instruction_builder_class: type[InstructionBuilder], curve_manager_class: type[CurveManager], - event_parser_class: type[EventParser] + event_parser_class: type[EventParser], ) -> None: """Register platform implementations. - + Args: platform: Platform enum value address_provider_class: AddressProvider implementation class @@ -56,114 +57,119 @@ class PlatformRegistry: event_parser_class: EventParser implementation class """ self._implementations[platform] = { - 'address_provider': address_provider_class, - 'instruction_builder': instruction_builder_class, - 'curve_manager': curve_manager_class, - 'event_parser': event_parser_class + "address_provider": address_provider_class, + "instruction_builder": instruction_builder_class, + "curve_manager": curve_manager_class, + "event_parser": event_parser_class, } - + def create_platform_implementations( - self, - platform: Platform, - client: SolanaClient, - **kwargs: Any + self, platform: Platform, client: SolanaClient, **kwargs: Any ) -> PlatformImplementations: """Create platform implementation instances with IDL support. - + Args: platform: Platform to create implementations for client: Solana RPC client **kwargs: Additional arguments for implementation constructors - + Returns: PlatformImplementations containing all interface implementations - + Raises: ValueError: If platform is not registered """ if platform not in self._implementations: raise ValueError(f"Platform {platform} is not registered") - + # Use client address as cache key to allow multiple clients cache_key = (platform, str(client.rpc_endpoint)) - + # Check if we already have instances for this platform + client combo if cache_key in self._instances: return self._instances[cache_key] - + impl_classes = self._implementations[platform] - + # Check if platform has IDL support and prepare IDL parser idl_parser = None if has_idl_support(platform): try: idl_manager = get_idl_manager() - idl_parser = idl_manager.get_parser(platform, verbose=kwargs.get('verbose_idl', False)) - logger.info(f"IDL parser loaded for {platform.value} platform implementations") + idl_parser = idl_manager.get_parser( + platform, verbose=kwargs.get("verbose_idl", False) + ) + logger.info( + f"IDL parser loaded for {platform.value} platform implementations" + ) except Exception as e: logger.warning(f"Failed to load IDL parser for {platform.value}: {e}") - + # Create instances - pass IDL parser to classes that need it - address_provider = impl_classes['address_provider']() - + address_provider = impl_classes["address_provider"]() + # For platforms with IDL support, pass the parser to relevant classes if idl_parser and platform in [Platform.LETS_BONK, Platform.PUMP_FUN]: - instruction_builder = impl_classes['instruction_builder'](idl_parser=idl_parser) - curve_manager = impl_classes['curve_manager'](client, idl_parser=idl_parser) - event_parser = impl_classes['event_parser'](idl_parser=idl_parser) + instruction_builder = impl_classes["instruction_builder"]( + idl_parser=idl_parser + ) + curve_manager = impl_classes["curve_manager"](client, idl_parser=idl_parser) + event_parser = impl_classes["event_parser"](idl_parser=idl_parser) else: # Fallback for platforms without IDL support - instruction_builder = impl_classes['instruction_builder']() - curve_manager = impl_classes['curve_manager'](client) - event_parser = impl_classes['event_parser']() - + instruction_builder = impl_classes["instruction_builder"]() + curve_manager = impl_classes["curve_manager"](client) + event_parser = impl_classes["event_parser"]() + implementations = PlatformImplementations( address_provider=address_provider, instruction_builder=instruction_builder, curve_manager=curve_manager, - event_parser=event_parser + event_parser=event_parser, ) - + # Cache the instances self._instances[cache_key] = implementations - + return implementations - - def get_platform_implementations(self, platform: Platform, client_endpoint: str) -> PlatformImplementations | None: + + def get_platform_implementations( + self, platform: Platform, client_endpoint: str + ) -> PlatformImplementations | None: """Get cached platform implementations. - + Args: platform: Platform to get implementations for client_endpoint: Client endpoint for cache lookup - + Returns: PlatformImplementations if available, None otherwise """ cache_key = (platform, client_endpoint) return self._instances.get(cache_key) - + def get_supported_platforms(self) -> list[Platform]: """Get list of supported platforms. - + Returns: List of registered platforms """ return list(self._implementations.keys()) - + def is_platform_supported(self, platform: Platform) -> bool: """Check if a platform is supported. - + Args: platform: Platform to check - + Returns: True if platform is registered, False otherwise """ return platform in self._implementations - + def clear_implementation_cache(self, platform: Platform | None = None) -> None: """Clear cached platform implementations. - + Args: platform: Specific platform to clear, or None to clear all """ @@ -171,7 +177,9 @@ class PlatformRegistry: logger.info("Clearing all cached platform implementations") self._instances.clear() else: - keys_to_remove = [key for key in self._instances.keys() if key[0] == platform] + keys_to_remove = [ + key for key in self._instances.keys() if key[0] == platform + ] for key in keys_to_remove: del self._instances[key] logger.info(f"Cleared cached implementations for {platform.value}") @@ -179,11 +187,11 @@ class PlatformRegistry: class PlatformFactory: """Factory for creating platform-specific implementations with IDL support.""" - + def __init__(self): self.registry = PlatformRegistry() self._setup_default_platforms() - + def _setup_default_platforms(self) -> None: """Setup default platform registrations.""" # Import and register pump.fun platform @@ -194,18 +202,18 @@ class PlatformFactory: PumpFunEventParser, PumpFunInstructionBuilder, ) - + self.registry.register_platform( Platform.PUMP_FUN, PumpFunAddressProvider, PumpFunInstructionBuilder, PumpFunCurveManager, - PumpFunEventParser + PumpFunEventParser, ) - + except ImportError as e: print(f"Warning: Could not register pump.fun platform: {e}") - + # Import and register LetsBonk platform try: from platforms.letsbonk import ( @@ -214,105 +222,116 @@ class PlatformFactory: LetsBonkEventParser, LetsBonkInstructionBuilder, ) - + self.registry.register_platform( Platform.LETS_BONK, LetsBonkAddressProvider, LetsBonkInstructionBuilder, LetsBonkCurveManager, - LetsBonkEventParser + LetsBonkEventParser, ) - + except ImportError as e: print(f"Warning: Could not register LetsBonk platform: {e}") - + def create_for_platform( - self, - platform: Platform, - client: SolanaClient, - **config: Any + self, platform: Platform, client: SolanaClient, **config: Any ) -> PlatformImplementations: """Create all implementations for a specific platform. - + Args: platform: Platform to create implementations for client: Solana RPC client **config: Platform-specific configuration (including verbose_idl) - + Returns: PlatformImplementations containing all interface implementations """ return self.registry.create_platform_implementations(platform, client, **config) - - def get_address_provider(self, platform: Platform, client: SolanaClient) -> AddressProvider: + + def get_address_provider( + self, platform: Platform, client: SolanaClient + ) -> AddressProvider: """Get address provider for platform. - + Args: platform: Platform to get provider for client: Solana RPC client - + Returns: AddressProvider implementation """ - implementations = self.registry.create_platform_implementations(platform, client) + implementations = self.registry.create_platform_implementations( + platform, client + ) return implementations.address_provider - - def get_instruction_builder(self, platform: Platform, client: SolanaClient) -> InstructionBuilder: + + def get_instruction_builder( + self, platform: Platform, client: SolanaClient + ) -> InstructionBuilder: """Get instruction builder for platform. - + Args: platform: Platform to get builder for client: Solana RPC client - + Returns: InstructionBuilder implementation """ - implementations = self.registry.create_platform_implementations(platform, client) + implementations = self.registry.create_platform_implementations( + platform, client + ) return implementations.instruction_builder - - def get_curve_manager(self, platform: Platform, client: SolanaClient) -> CurveManager: + + def get_curve_manager( + self, platform: Platform, client: SolanaClient + ) -> CurveManager: """Get curve manager for platform. - + Args: platform: Platform to get manager for client: Solana RPC client - + Returns: CurveManager implementation """ - implementations = self.registry.create_platform_implementations(platform, client) + implementations = self.registry.create_platform_implementations( + platform, client + ) return implementations.curve_manager - + def get_event_parser(self, platform: Platform, client: SolanaClient) -> EventParser: """Get event parser for platform. - + Args: platform: Platform to get parser for client: Solana RPC client - + Returns: EventParser implementation """ - implementations = self.registry.create_platform_implementations(platform, client) + implementations = self.registry.create_platform_implementations( + platform, client + ) return implementations.event_parser - + def get_supported_platforms(self) -> list[Platform]: """Get list of supported platforms. - + Returns: List of supported platforms """ return self.registry.get_supported_platforms() - + def clear_caches(self, platform: Platform | None = None) -> None: """Clear all caches for better memory management. - + Args: platform: Specific platform to clear, or None to clear all """ # Clear implementation cache self.registry.clear_implementation_cache(platform) - + # Clear IDL parser cache idl_manager = get_idl_manager() idl_manager.clear_cache(platform) @@ -322,13 +341,15 @@ class PlatformFactory: platform_factory = PlatformFactory() -def get_platform_implementations(platform: Platform, client: SolanaClient) -> PlatformImplementations: +def get_platform_implementations( + platform: Platform, client: SolanaClient +) -> PlatformImplementations: """Convenience function to get platform implementations. - + Args: platform: Platform to get implementations for client: Solana RPC client - + Returns: PlatformImplementations containing all interface implementations """ @@ -340,10 +361,10 @@ def register_platform_implementations( address_provider_class: type[AddressProvider], instruction_builder_class: type[InstructionBuilder], curve_manager_class: type[CurveManager], - event_parser_class: type[EventParser] + event_parser_class: type[EventParser], ) -> None: """Register platform implementations with the global factory. - + Args: platform: Platform enum value address_provider_class: AddressProvider implementation class @@ -356,5 +377,5 @@ def register_platform_implementations( address_provider_class, instruction_builder_class, curve_manager_class, - event_parser_class - ) \ No newline at end of file + event_parser_class, + ) diff --git a/src/platforms/letsbonk/__init__.py b/src/platforms/letsbonk/__init__.py index 110962b..c984351 100644 --- a/src/platforms/letsbonk/__init__.py +++ b/src/platforms/letsbonk/__init__.py @@ -13,9 +13,9 @@ from .pumpportal_processor import LetsBonkPumpPortalProcessor # Export implementations for direct use if needed __all__ = [ - 'LetsBonkAddressProvider', - 'LetsBonkCurveManager', - 'LetsBonkEventParser', - 'LetsBonkInstructionBuilder', - 'LetsBonkPumpPortalProcessor' -] \ No newline at end of file + "LetsBonkAddressProvider", + "LetsBonkCurveManager", + "LetsBonkEventParser", + "LetsBonkInstructionBuilder", + "LetsBonkPumpPortalProcessor", +] diff --git a/src/platforms/letsbonk/address_provider.py b/src/platforms/letsbonk/address_provider.py index 37d392d..90f0ff1 100644 --- a/src/platforms/letsbonk/address_provider.py +++ b/src/platforms/letsbonk/address_provider.py @@ -18,35 +18,41 @@ from interfaces.core import AddressProvider, Platform, TokenInfo @dataclass class LetsBonkAddresses: """LetsBonk (Raydium LaunchLab) program addresses.""" - + # Raydium LaunchLab program addresses - PROGRAM: Final[Pubkey] = Pubkey.from_string("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj") - GLOBAL_CONFIG: Final[Pubkey] = Pubkey.from_string("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX") - PLATFORM_CONFIG: Final[Pubkey] = Pubkey.from_string("FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1") + PROGRAM: Final[Pubkey] = Pubkey.from_string( + "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj" + ) + GLOBAL_CONFIG: Final[Pubkey] = Pubkey.from_string( + "6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX" + ) + PLATFORM_CONFIG: Final[Pubkey] = Pubkey.from_string( + "FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1" + ) class LetsBonkAddressProvider(AddressProvider): """LetsBonk (Raydium LaunchLab) implementation of AddressProvider interface.""" - + @property def platform(self) -> Platform: """Get the platform this provider serves.""" return Platform.LETS_BONK - + @property def program_id(self) -> Pubkey: """Get the main program ID for this platform.""" return LetsBonkAddresses.PROGRAM - + def get_system_addresses(self) -> dict[str, Pubkey]: """Get all system addresses required for LetsBonk. - + Returns: Dictionary mapping address names to Pubkey objects """ # Get system addresses from the single source of truth system_addresses = SystemAddresses.get_all_system_addresses() - + # Add LetsBonk specific addresses letsbonk_addresses = { # Raydium LaunchLab specific addresses @@ -54,179 +60,184 @@ class LetsBonkAddressProvider(AddressProvider): "global_config": LetsBonkAddresses.GLOBAL_CONFIG, "platform_config": LetsBonkAddresses.PLATFORM_CONFIG, } - + # Combine system and platform-specific addresses return {**system_addresses, **letsbonk_addresses} - - def derive_pool_address(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey: + + def derive_pool_address( + self, base_mint: Pubkey, quote_mint: Pubkey | None = None + ) -> Pubkey: """Derive the pool state address for a token pair. - + For LetsBonk, this derives the pool state PDA using base_mint and WSOL. - + Args: base_mint: Base token mint address quote_mint: Quote token mint (defaults to WSOL) - + Returns: Pool state address """ if quote_mint is None: quote_mint = SystemAddresses.SOL_MINT - + pool_state, _ = Pubkey.find_program_address( - [b"pool", bytes(base_mint), bytes(quote_mint)], - LetsBonkAddresses.PROGRAM + [b"pool", bytes(base_mint), bytes(quote_mint)], LetsBonkAddresses.PROGRAM ) return pool_state - - def derive_base_vault(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey: + + def derive_base_vault( + self, base_mint: Pubkey, quote_mint: Pubkey | None = None + ) -> Pubkey: """Derive the base vault address for a token pair. - + Args: base_mint: Base token mint address quote_mint: Quote token mint (defaults to WSOL) - + Returns: Base vault address """ if quote_mint is None: quote_mint = SystemAddresses.SOL_MINT - + # First derive the pool state address pool_state = self.derive_pool_address(base_mint, quote_mint) - + # Then derive the base vault using pool_vault seed base_vault, _ = Pubkey.find_program_address( [b"pool_vault", bytes(pool_state), bytes(base_mint)], - LetsBonkAddresses.PROGRAM + LetsBonkAddresses.PROGRAM, ) return base_vault - - def derive_quote_vault(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey: + + def derive_quote_vault( + self, base_mint: Pubkey, quote_mint: Pubkey | None = None + ) -> Pubkey: """Derive the quote vault address for a token pair. - + Args: base_mint: Base token mint address quote_mint: Quote token mint (defaults to WSOL) - + Returns: Quote vault address """ if quote_mint is None: quote_mint = SystemAddresses.SOL_MINT - + # First derive the pool state address pool_state = self.derive_pool_address(base_mint, quote_mint) - + # Then derive the quote vault using pool_vault seed quote_vault, _ = Pubkey.find_program_address( [b"pool_vault", bytes(pool_state), bytes(quote_mint)], - LetsBonkAddresses.PROGRAM + LetsBonkAddresses.PROGRAM, ) return quote_vault - + def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey: """Derive user's associated token account address. - + Args: user: User's wallet address mint: Token mint address - + Returns: User's associated token account address """ return get_associated_token_address(user, mint) - + def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]: """Get LetsBonk-specific additional accounts needed for trading. - + Args: token_info: Token information - + Returns: Dictionary of additional account addresses """ accounts = {} - + # Add pool state - derive if not present or use existing if token_info.pool_state: accounts["pool_state"] = token_info.pool_state else: accounts["pool_state"] = self.derive_pool_address(token_info.mint) - + # Add vault addresses - derive if not present or use existing if token_info.base_vault: accounts["base_vault"] = token_info.base_vault else: accounts["base_vault"] = self.derive_base_vault(token_info.mint) - + if token_info.quote_vault: accounts["quote_vault"] = token_info.quote_vault else: accounts["quote_vault"] = self.derive_quote_vault(token_info.mint) - + # Derive authority PDA accounts["authority"] = self.derive_authority_pda() - + # Derive event authority PDA accounts["event_authority"] = self.derive_event_authority_pda() - + return accounts - + def derive_authority_pda(self) -> Pubkey: """Derive the authority PDA for Raydium LaunchLab. - + This PDA acts as the authority for pool vault operations. - + Returns: Authority PDA address """ AUTH_SEED = b"vault_auth_seed" authority_pda, _ = Pubkey.find_program_address( - [AUTH_SEED], - LetsBonkAddresses.PROGRAM + [AUTH_SEED], LetsBonkAddresses.PROGRAM ) return authority_pda - + def derive_event_authority_pda(self) -> Pubkey: """Derive the event authority PDA for Raydium LaunchLab. - + This PDA is used for emitting program events during swaps. - + Returns: Event authority PDA address """ EVENT_AUTHORITY_SEED = b"__event_authority" event_authority_pda, _ = Pubkey.find_program_address( - [EVENT_AUTHORITY_SEED], - LetsBonkAddresses.PROGRAM + [EVENT_AUTHORITY_SEED], LetsBonkAddresses.PROGRAM ) return event_authority_pda - + def create_wsol_account_with_seed(self, payer: Pubkey, seed: str) -> Pubkey: """Create a WSOL account address using createAccountWithSeed pattern. - + Args: payer: The account that will pay for and own the new account seed: String seed for deterministic account generation - + Returns: New WSOL account address """ return Pubkey.create_with_seed(payer, seed, SystemAddresses.TOKEN_PROGRAM) - - def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]: + + def get_buy_instruction_accounts( + self, token_info: TokenInfo, user: Pubkey + ) -> dict[str, Pubkey]: """Get all accounts needed for a buy instruction. - + Args: token_info: Token information user: User's wallet address - + Returns: Dictionary of account addresses for buy instruction """ additional_accounts = self.get_additional_accounts(token_info) - + return { "payer": user, "authority": additional_accounts["authority"], @@ -243,19 +254,21 @@ class LetsBonkAddressProvider(AddressProvider): "event_authority": additional_accounts["event_authority"], "program": LetsBonkAddresses.PROGRAM, } - - def get_sell_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]: + + def get_sell_instruction_accounts( + self, token_info: TokenInfo, user: Pubkey + ) -> dict[str, Pubkey]: """Get all accounts needed for a sell instruction. - + Args: token_info: Token information user: User's wallet address - + Returns: Dictionary of account addresses for sell instruction """ additional_accounts = self.get_additional_accounts(token_info) - + return { "payer": user, "authority": additional_accounts["authority"], @@ -272,14 +285,16 @@ class LetsBonkAddressProvider(AddressProvider): "event_authority": additional_accounts["event_authority"], "program": LetsBonkAddresses.PROGRAM, } - - def get_wsol_account_creation_accounts(self, user: Pubkey, wsol_account: Pubkey) -> dict[str, Pubkey]: + + def get_wsol_account_creation_accounts( + self, user: Pubkey, wsol_account: Pubkey + ) -> dict[str, Pubkey]: """Get accounts needed for WSOL account creation and initialization. - + Args: user: User's wallet address wsol_account: WSOL account to be created - + Returns: Dictionary of account addresses for WSOL operations """ @@ -291,4 +306,4 @@ class LetsBonkAddressProvider(AddressProvider): "system_program": SystemAddresses.SYSTEM_PROGRAM, "token_program": SystemAddresses.TOKEN_PROGRAM, "rent": SystemAddresses.RENT, - } \ No newline at end of file + } diff --git a/src/platforms/letsbonk/curve_manager.py b/src/platforms/letsbonk/curve_manager.py index 4f49c3f..4bcf510 100644 --- a/src/platforms/letsbonk/curve_manager.py +++ b/src/platforms/letsbonk/curve_manager.py @@ -21,10 +21,10 @@ logger = get_logger(__name__) class LetsBonkCurveManager(CurveManager): """LetsBonk (Raydium LaunchLab) implementation of CurveManager interface.""" - + def __init__(self, client: SolanaClient, idl_parser: IDLParser): """Initialize LetsBonk curve manager with injected IDL parser. - + Args: client: Solana RPC client idl_parser: Pre-loaded IDL parser for LetsBonk platform @@ -32,20 +32,20 @@ class LetsBonkCurveManager(CurveManager): self.client = client self.address_provider = LetsBonkAddressProvider() self._idl_parser = idl_parser - + logger.info("LetsBonk curve manager initialized with injected IDL parser") - + @property def platform(self) -> Platform: """Get the platform this manager serves.""" return Platform.LETS_BONK - + async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]: """Get the current state of a LetsBonk pool. - + Args: pool_address: Address of the pool state account - + Returns: Dictionary containing pool state data """ @@ -53,136 +53,130 @@ class LetsBonkCurveManager(CurveManager): account = await self.client.get_account_info(pool_address) if not account.data: raise ValueError(f"No data in pool state account {pool_address}") - + # Decode pool state using injected IDL parser pool_state_data = self._decode_pool_state_with_idl(account.data) - + return pool_state_data - + except Exception as e: logger.exception("Failed to get pool state") raise ValueError(f"Invalid pool state: {e!s}") - + async def calculate_price(self, pool_address: Pubkey) -> float: """Calculate current token price from pool state. - + Args: pool_address: Address of the pool state - + Returns: Current token price in SOL """ pool_state = await self.get_pool_state(pool_address) - + # Use virtual reserves for price calculation virtual_base = pool_state["virtual_base"] virtual_quote = pool_state["virtual_quote"] - + if virtual_base <= 0 or virtual_quote <= 0: raise ValueError("Invalid reserve state") - + # Price = quote_reserves / base_reserves (how much SOL per token) price_lamports = virtual_quote / virtual_base price_sol = price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL - + return price_sol - + async def calculate_buy_amount_out( - self, - pool_address: Pubkey, - amount_in: int + self, pool_address: Pubkey, amount_in: int ) -> int: """Calculate expected tokens received for a buy operation. - + Uses the constant product AMM formula. - + Args: pool_address: Address of the pool state amount_in: Amount of SOL to spend (in lamports) - + Returns: Expected amount of tokens to receive (in raw token units) """ pool_state = await self.get_pool_state(pool_address) - + virtual_base = pool_state["virtual_base"] virtual_quote = pool_state["virtual_quote"] - + # Constant product formula: tokens_out = (amount_in * virtual_base) / (virtual_quote + amount_in) numerator = amount_in * virtual_base denominator = virtual_quote + amount_in - + if denominator == 0: return 0 - + tokens_out = numerator // denominator return tokens_out - + async def calculate_sell_amount_out( - self, - pool_address: Pubkey, - amount_in: int + self, pool_address: Pubkey, amount_in: int ) -> int: """Calculate expected SOL received for a sell operation. - + Uses the constant product AMM formula. - + Args: pool_address: Address of the pool state amount_in: Amount of tokens to sell (in raw token units) - + Returns: Expected amount of SOL to receive (in lamports) """ pool_state = await self.get_pool_state(pool_address) - + virtual_base = pool_state["virtual_base"] virtual_quote = pool_state["virtual_quote"] - + # Constant product formula: sol_out = (amount_in * virtual_quote) / (virtual_base + amount_in) numerator = amount_in * virtual_quote denominator = virtual_base + amount_in - + if denominator == 0: return 0 - + sol_out = numerator // denominator return sol_out - + async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]: """Get current pool reserves. - + Args: pool_address: Address of the pool state - + Returns: Tuple of (base_reserves, quote_reserves) in raw units """ pool_state = await self.get_pool_state(pool_address) return (pool_state["virtual_base"], pool_state["virtual_quote"]) - + def _decode_pool_state_with_idl(self, data: bytes) -> dict[str, Any]: """Decode pool state data using injected IDL parser. - + Args: data: Raw account data - + Returns: Dictionary with decoded pool state - + Raises: ValueError: If IDL parsing fails """ # Use injected IDL parser to decode PoolState account data decoded_pool_state = self._idl_parser.decode_account_data( - data, - "PoolState", - skip_discriminator=True + data, "PoolState", skip_discriminator=True ) - + if not decoded_pool_state: raise ValueError("Failed to decode pool state with IDL parser") - + # Extract the fields we need for trading calculations # Based on the PoolState structure from the IDL pool_data = { @@ -193,28 +187,31 @@ class LetsBonkCurveManager(CurveManager): "status": decoded_pool_state.get("status", 0), "supply": decoded_pool_state.get("supply", 0), } - + # Calculate additional metrics if pool_data["virtual_base"] > 0: pool_data["price_per_token"] = ( - (pool_data["virtual_quote"] / pool_data["virtual_base"]) - * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL + (pool_data["virtual_quote"] / pool_data["virtual_base"]) + * (10**TOKEN_DECIMALS) + / LAMPORTS_PER_SOL ) else: pool_data["price_per_token"] = 0 - - logger.debug(f"Decoded pool state: virtual_base={pool_data['virtual_base']}, " - f"virtual_quote={pool_data['virtual_quote']}, " - f"price={pool_data['price_per_token']:.8f} SOL") - + + logger.debug( + f"Decoded pool state: virtual_base={pool_data['virtual_base']}, " + f"virtual_quote={pool_data['virtual_quote']}, " + f"price={pool_data['price_per_token']:.8f} SOL" + ) + return pool_data - + async def validate_pool_state_structure(self, pool_address: Pubkey) -> bool: """Validate that the pool state structure matches IDL expectations. - + Args: pool_address: Address of the pool state - + Returns: True if structure is valid, False otherwise """ @@ -222,23 +219,27 @@ class LetsBonkCurveManager(CurveManager): # This would be used during development/testing to ensure # the IDL parsing is working correctly pool_state = await self.get_pool_state(pool_address) - + required_fields = [ - "virtual_base", "virtual_quote", - "real_base", "real_quote" + "virtual_base", + "virtual_quote", + "real_base", + "real_quote", ] - + for field in required_fields: if field not in pool_state: logger.error(f"Missing required field: {field}") return False - + if not isinstance(pool_state[field], int): - logger.error(f"Field {field} is not an integer: {type(pool_state[field])}") + logger.error( + f"Field {field} is not an integer: {type(pool_state[field])}" + ) return False - + return True - + except Exception: logger.exception("Pool state validation failed") - return False \ No newline at end of file + return False diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py index f5b4b1b..5010429 100644 --- a/src/platforms/letsbonk/event_parser.py +++ b/src/platforms/letsbonk/event_parser.py @@ -23,59 +23,56 @@ logger = get_logger(__name__) class LetsBonkEventParser(EventParser): """LetsBonk implementation of EventParser interface with IDL-based parsing.""" - + def __init__(self, idl_parser: IDLParser): """Initialize LetsBonk event parser with injected IDL parser. - + Args: idl_parser: Pre-loaded IDL parser for LetsBonk platform """ self.address_provider = LetsBonkAddressProvider() self._idl_parser = idl_parser - + # Get discriminators from injected IDL parser discriminators = self._idl_parser.get_instruction_discriminators() self._initialize_discriminator_bytes = discriminators["initialize"] - self._initialize_discriminator = struct.unpack(" Platform: """Get the platform this parser serves.""" return Platform.LETS_BONK - + def parse_token_creation_from_logs( - self, - logs: list[str], - signature: str + self, logs: list[str], signature: str ) -> TokenInfo | None: """Parse token creation from LetsBonk transaction logs. - + Args: logs: List of log strings from transaction signature: Transaction signature - + Returns: TokenInfo if token creation found, None otherwise """ # LetsBonk doesn't emit specific logs for token creation like pump.fun # Token creation is identified through instruction parsing return None - + def parse_token_creation_from_instruction( - self, - instruction_data: bytes, - accounts: list[int], - account_keys: list[bytes] + self, instruction_data: bytes, accounts: list[int], account_keys: list[bytes] ) -> TokenInfo | None: """Parse token creation from LetsBonk instruction data using injected IDL parser. - + Args: instruction_data: Raw instruction data accounts: List of account indices account_keys: List of account public keys - + Returns: TokenInfo if token creation found, None otherwise """ @@ -93,14 +90,16 @@ class LetsBonkEventParser(EventParser): return Pubkey.from_bytes(account_keys[account_index]) # Parse instruction data using injected IDL parser - decoded = self._idl_parser.decode_instruction(instruction_data, account_keys, accounts) - if not decoded or decoded['instruction_name'] != 'initialize': + decoded = self._idl_parser.decode_instruction( + instruction_data, account_keys, accounts + ) + if not decoded or decoded["instruction_name"] != "initialize": return None - - args = decoded.get('args', {}) - + + args = decoded.get("args", {}) + # Extract MintParams from the decoded arguments - base_mint_param = args.get('base_mint_param', {}) + base_mint_param = args.get("base_mint_param", {}) if not base_mint_param: return None @@ -109,7 +108,7 @@ class LetsBonkEventParser(EventParser): # 0: creator (signer) # 1: creator_ata (not needed for TokenInfo) # 2: global_config - # 3: platform_config + # 3: platform_config # 4: creator # 5: pool_state # 6: base_mint @@ -117,7 +116,7 @@ class LetsBonkEventParser(EventParser): # 8: base_vault # 9: quote_vault # ... other accounts - + creator = get_account_key(0) # First signer account (creator) pool_state = get_account_key(5) # pool_state account base_mint = get_account_key(6) # base_mint account @@ -125,8 +124,10 @@ class LetsBonkEventParser(EventParser): quote_vault = get_account_key(9) # quote_vault account if not all([creator, pool_state, base_mint, base_vault, quote_vault]): - logger.debug(f"Missing required accounts: creator={creator}, pool_state={pool_state}, " - f"base_mint={base_mint}, base_vault={base_vault}, quote_vault={quote_vault}") + logger.debug( + f"Missing required accounts: creator={creator}, pool_state={pool_state}, " + f"base_mint={base_mint}, base_vault={base_vault}, quote_vault={quote_vault}" + ) return None return TokenInfo( @@ -146,21 +147,20 @@ class LetsBonkEventParser(EventParser): except Exception as e: logger.debug(f"Failed to parse initialize instruction: {e}") return None - + def parse_token_creation_from_geyser( - self, - transaction_info: Any + self, transaction_info: Any ) -> TokenInfo | None: """Parse token creation from Geyser transaction data. - + Args: transaction_info: Geyser transaction information - + Returns: TokenInfo if token creation found, None otherwise """ try: - if not hasattr(transaction_info, 'transaction'): + if not hasattr(transaction_info, "transaction"): return None tx = transaction_info.transaction.transaction.transaction @@ -183,10 +183,14 @@ class LetsBonkEventParser(EventParser): for acc_idx in ix.accounts: if acc_idx < len(msg.account_keys): acc_key = msg.account_keys[acc_idx] - if bytes(acc_key) == bytes(self.address_provider.get_system_addresses()["platform_config"]): + if bytes(acc_key) == bytes( + self.address_provider.get_system_addresses()[ + "platform_config" + ] + ): has_platform_config = True break - + if not has_platform_config: continue @@ -202,29 +206,29 @@ class LetsBonkEventParser(EventParser): except Exception as e: logger.debug(f"Failed to parse geyser transaction: {e}") return None - + def get_program_id(self) -> Pubkey: """Get the Raydium LaunchLab program ID this parser monitors. - + Returns: Raydium LaunchLab program ID """ return self.address_provider.program_id - + def get_instruction_discriminators(self) -> list[bytes]: """Get instruction discriminators for token creation. - + Returns: List of discriminator bytes to match """ return [self._initialize_discriminator_bytes] - + def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None: """Parse token creation from block data (for block listener). - + Args: block_data: Block data from WebSocket - + Returns: TokenInfo if token creation found, None otherwise """ @@ -243,82 +247,95 @@ class LetsBonkEventParser(EventParser): tx_data_encoded = tx_data[0] tx_data_decoded = base64.b64decode(tx_data_encoded) transaction = VersionedTransaction.from_bytes(tx_data_decoded) - + for ix in transaction.message.instructions: - program_id = transaction.message.account_keys[ix.program_id_index] - + program_id = transaction.message.account_keys[ + ix.program_id_index + ] + # Check if instruction is from LetsBonk program if str(program_id) != str(self.get_program_id()): continue ix_data = bytes(ix.data) - + # Check for initialize discriminator if len(ix_data) >= 8: discriminator = struct.unpack("= len(message["accountKeys"]): continue - + program_id_str = message["accountKeys"][program_idx] if program_id_str != str(self.get_program_id()): continue - + # Decode instruction data ix_data = base64.b64decode(ix["data"]) - + if len(ix_data) >= 8: discriminator = struct.unpack(" Platform: """Get the platform this builder serves.""" return Platform.LETS_BONK - + async def build_buy_instruction( self, token_info: TokenInfo, user: Pubkey, amount_in: int, minimum_amount_out: int, - address_provider: AddressProvider + address_provider: AddressProvider, ) -> list[Instruction]: """Build buy instruction(s) for LetsBonk using buy_exact_in. - + Args: token_info: Token information user: User's wallet address amount_in: Amount of SOL to spend (in lamports) minimum_amount_out: Minimum tokens expected (raw token units) address_provider: Platform address provider - + Returns: List of instructions needed for the buy operation """ instructions = [] - + # Get all required accounts accounts_info = address_provider.get_buy_instruction_accounts(token_info, user) - + # 1. Create idempotent ATA for base token ata_instruction = create_idempotent_associated_token_account( user, # payer - user, # owner + user, # owner token_info.mint, # mint SystemAddresses.TOKEN_PROGRAM, # token program ) instructions.append(ata_instruction) - + # 2. Create WSOL account with seed (temporary account for the transaction) wsol_seed = self._generate_wsol_seed(user) wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed) - + # Account creation cost + amount to spend account_creation_lamports = TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE total_lamports = amount_in + account_creation_lamports - + create_wsol_ix = create_account_with_seed( CreateAccountWithSeedParams( from_pubkey=user, @@ -100,97 +100,129 @@ class LetsBonkInstructionBuilder(InstructionBuilder): seed=wsol_seed, lamports=total_lamports, space=TOKEN_ACCOUNT_SIZE, - owner=SystemAddresses.TOKEN_PROGRAM + owner=SystemAddresses.TOKEN_PROGRAM, ) ) instructions.append(create_wsol_ix) - + # 3. Initialize WSOL account initialize_wsol_ix = self._create_initialize_account_instruction( - wsol_account, - SystemAddresses.SOL_MINT, - user + wsol_account, SystemAddresses.SOL_MINT, user ) instructions.append(initialize_wsol_ix) - + # 4. Build buy_exact_in instruction with correct account ordering # Based on the IDL and manual examples, the account order should be: buy_accounts = [ - AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer - AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), # authority - AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), # global_config - AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), # platform_config - AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), # pool_state - AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), # user_base_token - AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token (WSOL account) - AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), # base_vault - AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), # quote_vault - AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), # base_token_mint - AccountMeta(pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False), # quote_token_mint - AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # base_token_program - AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # quote_token_program - AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), # event_authority - AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), # program + AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer + AccountMeta( + pubkey=accounts_info["authority"], is_signer=False, is_writable=False + ), # authority + AccountMeta( + pubkey=accounts_info["global_config"], + is_signer=False, + is_writable=False, + ), # global_config + AccountMeta( + pubkey=accounts_info["platform_config"], + is_signer=False, + is_writable=False, + ), # platform_config + AccountMeta( + pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True + ), # pool_state + AccountMeta( + pubkey=accounts_info["user_base_token"], + is_signer=False, + is_writable=True, + ), # user_base_token + AccountMeta( + pubkey=wsol_account, is_signer=False, is_writable=True + ), # user_quote_token (WSOL account) + AccountMeta( + pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True + ), # base_vault + AccountMeta( + pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True + ), # quote_vault + AccountMeta( + pubkey=token_info.mint, is_signer=False, is_writable=False + ), # base_token_mint + AccountMeta( + pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False + ), # quote_token_mint + AccountMeta( + pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False + ), # base_token_program + AccountMeta( + pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False + ), # quote_token_program + AccountMeta( + pubkey=accounts_info["event_authority"], + is_signer=False, + is_writable=False, + ), # event_authority + AccountMeta( + pubkey=accounts_info["program"], is_signer=False, is_writable=False + ), # program ] - + # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate SHARE_FEE_RATE = 0 # No sharing fee instruction_data = ( - self._buy_exact_in_discriminator + - struct.pack(" list[Instruction]: """Build sell instruction(s) for LetsBonk using sell_exact_in. - + Args: token_info: Token information user: User's wallet address amount_in: Amount of tokens to sell (raw token units) minimum_amount_out: Minimum SOL expected (in lamports) address_provider: Platform address provider - + Returns: List of instructions needed for the sell operation """ instructions = [] - + # Get all required accounts accounts_info = address_provider.get_sell_instruction_accounts(token_info, user) - + # 1. Create WSOL account with seed (to receive SOL) wsol_seed = self._generate_wsol_seed(user) wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed) - + # Minimal account creation cost account_creation_lamports = TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE - + create_wsol_ix = create_account_with_seed( CreateAccountWithSeedParams( from_pubkey=user, @@ -199,82 +231,111 @@ class LetsBonkInstructionBuilder(InstructionBuilder): seed=wsol_seed, lamports=account_creation_lamports, space=TOKEN_ACCOUNT_SIZE, - owner=SystemAddresses.TOKEN_PROGRAM + owner=SystemAddresses.TOKEN_PROGRAM, ) ) instructions.append(create_wsol_ix) - + # 2. Initialize WSOL account initialize_wsol_ix = self._create_initialize_account_instruction( - wsol_account, - SystemAddresses.SOL_MINT, - user + wsol_account, SystemAddresses.SOL_MINT, user ) instructions.append(initialize_wsol_ix) - + # 3. Build sell_exact_in instruction with correct account ordering sell_accounts = [ - AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer - AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), # authority - AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), # global_config - AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), # platform_config - AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), # pool_state - AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), # user_base_token (tokens being sold) - AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token (WSOL received) - AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), # base_vault (receives tokens) - AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), # quote_vault (sends WSOL) - AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), # base_token_mint - AccountMeta(pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False), # quote_token_mint - AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # base_token_program - AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # quote_token_program - AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), # event_authority - AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), # program + AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer + AccountMeta( + pubkey=accounts_info["authority"], is_signer=False, is_writable=False + ), # authority + AccountMeta( + pubkey=accounts_info["global_config"], + is_signer=False, + is_writable=False, + ), # global_config + AccountMeta( + pubkey=accounts_info["platform_config"], + is_signer=False, + is_writable=False, + ), # platform_config + AccountMeta( + pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True + ), # pool_state + AccountMeta( + pubkey=accounts_info["user_base_token"], + is_signer=False, + is_writable=True, + ), # user_base_token (tokens being sold) + AccountMeta( + pubkey=wsol_account, is_signer=False, is_writable=True + ), # user_quote_token (WSOL received) + AccountMeta( + pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True + ), # base_vault (receives tokens) + AccountMeta( + pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True + ), # quote_vault (sends WSOL) + AccountMeta( + pubkey=token_info.mint, is_signer=False, is_writable=False + ), # base_token_mint + AccountMeta( + pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False + ), # quote_token_mint + AccountMeta( + pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False + ), # base_token_program + AccountMeta( + pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False + ), # quote_token_program + AccountMeta( + pubkey=accounts_info["event_authority"], + is_signer=False, + is_writable=False, + ), # event_authority + AccountMeta( + pubkey=accounts_info["program"], is_signer=False, is_writable=False + ), # program ] - + # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate SHARE_FEE_RATE = 0 # No sharing fee instruction_data = ( - self._sell_exact_in_discriminator + - struct.pack(" list[Pubkey]: """Get list of accounts required for buy operation (for priority fee calculation). - + Args: token_info: Token information user: User's wallet address address_provider: Platform address provider - + Returns: List of account addresses that will be accessed """ accounts_info = address_provider.get_buy_instruction_accounts(token_info, user) - + return [ accounts_info["pool_state"], accounts_info["user_base_token"], @@ -284,25 +345,22 @@ class LetsBonkInstructionBuilder(InstructionBuilder): SystemAddresses.SOL_MINT, accounts_info["program"], ] - + def get_required_accounts_for_sell( - self, - token_info: TokenInfo, - user: Pubkey, - address_provider: AddressProvider + self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider ) -> list[Pubkey]: """Get list of accounts required for sell operation (for priority fee calculation). - + Args: token_info: Token information user: User's wallet address address_provider: Platform address provider - + Returns: List of account addresses that will be accessed """ accounts_info = address_provider.get_sell_instruction_accounts(token_info, user) - + return [ accounts_info["pool_state"], accounts_info["user_base_token"], @@ -312,33 +370,30 @@ class LetsBonkInstructionBuilder(InstructionBuilder): SystemAddresses.SOL_MINT, accounts_info["program"], ] - + def _generate_wsol_seed(self, user: Pubkey) -> str: """Generate a unique seed for WSOL account creation. - + Args: user: User's wallet address - + Returns: Unique seed string for WSOL account """ # Generate a unique seed based on timestamp and user pubkey seed_data = f"{int(time.time())}{user!s}" return hashlib.sha256(seed_data.encode()).hexdigest()[:32] - + def _create_initialize_account_instruction( - self, - account: Pubkey, - mint: Pubkey, - owner: Pubkey + self, account: Pubkey, mint: Pubkey, owner: Pubkey ) -> Instruction: """Create an InitializeAccount instruction for the Token Program. - + Args: account: The account to initialize mint: The token mint owner: The account owner - + Returns: Instruction for initializing the account """ @@ -346,31 +401,28 @@ class LetsBonkInstructionBuilder(InstructionBuilder): AccountMeta(pubkey=account, is_signer=False, is_writable=True), AccountMeta(pubkey=mint, is_signer=False, is_writable=False), AccountMeta(pubkey=owner, is_signer=False, is_writable=False), - AccountMeta(pubkey=SystemAddresses.RENT, is_signer=False, is_writable=False), + AccountMeta( + pubkey=SystemAddresses.RENT, is_signer=False, is_writable=False + ), ] - + # InitializeAccount instruction discriminator (instruction 1 in Token Program) data = bytes([1]) - + return Instruction( - program_id=SystemAddresses.TOKEN_PROGRAM, - data=data, - accounts=accounts + program_id=SystemAddresses.TOKEN_PROGRAM, data=data, accounts=accounts ) - + def _create_close_account_instruction( - self, - account: Pubkey, - destination: Pubkey, - owner: Pubkey + self, account: Pubkey, destination: Pubkey, owner: Pubkey ) -> Instruction: """Create a CloseAccount instruction for the Token Program. - + Args: account: The account to close destination: Where to send the remaining lamports owner: The account owner (must sign) - + Returns: Instruction for closing the account """ @@ -379,34 +431,32 @@ class LetsBonkInstructionBuilder(InstructionBuilder): AccountMeta(pubkey=destination, is_signer=False, is_writable=True), AccountMeta(pubkey=owner, is_signer=True, is_writable=False), ] - + # CloseAccount instruction discriminator (instruction 9 in Token Program) data = bytes([9]) - + return Instruction( - program_id=SystemAddresses.TOKEN_PROGRAM, - data=data, - accounts=accounts + program_id=SystemAddresses.TOKEN_PROGRAM, data=data, accounts=accounts ) - + def calculate_token_amount_raw(self, token_amount_decimal: float) -> int: """Convert decimal token amount to raw token units. - + Args: token_amount_decimal: Token amount in decimal form - + Returns: Token amount in raw units (adjusted for decimals) """ return int(token_amount_decimal * 10**TOKEN_DECIMALS) - + def calculate_token_amount_decimal(self, token_amount_raw: int) -> float: """Convert raw token amount to decimal form. - + Args: token_amount_raw: Token amount in raw units - + Returns: Token amount in decimal form """ - return token_amount_raw / 10**TOKEN_DECIMALS \ No newline at end of file + return token_amount_raw / 10**TOKEN_DECIMALS diff --git a/src/platforms/letsbonk/pumpportal_processor.py b/src/platforms/letsbonk/pumpportal_processor.py index ce03b08..f29d9aa 100644 --- a/src/platforms/letsbonk/pumpportal_processor.py +++ b/src/platforms/letsbonk/pumpportal_processor.py @@ -14,39 +14,39 @@ logger = get_logger(__name__) class LetsBonkPumpPortalProcessor: """PumpPortal processor for LetsBonk tokens.""" - + def __init__(self): """Initialize the processor with address provider.""" self.address_provider = LetsBonkAddressProvider() - + @property def platform(self) -> Platform: """Get the platform this processor handles.""" return Platform.LETS_BONK - + @property def supported_pool_names(self) -> list[str]: """Get the pool names this processor supports from PumpPortal.""" return ["bonk"] # PumpPortal pool name for LetsBonk/bonk pools - + def can_process(self, token_data: dict) -> bool: """Check if this processor can handle the given token data. - + Args: token_data: Token data from PumpPortal - + Returns: True if this processor can handle the token data """ pool = token_data.get("pool", "").lower() return pool in self.supported_pool_names - + def process_token_data(self, token_data: dict) -> TokenInfo | None: """Process LetsBonk token data from PumpPortal. - + Args: token_data: Token data from PumpPortal WebSocket - + Returns: TokenInfo if token creation found, None otherwise """ @@ -62,7 +62,9 @@ class LetsBonkPumpPortalProcessor: # This would need to be adjusted based on actual PumpPortal data for LetsBonk tokens if not all([name, symbol, mint_str, creator_str]): - logger.warning("Missing required fields in PumpPortal LetsBonk token data") + logger.warning( + "Missing required fields in PumpPortal LetsBonk token data" + ) return None # Convert string addresses to Pubkey objects @@ -72,7 +74,7 @@ class LetsBonkPumpPortalProcessor: # Derive LetsBonk-specific addresses pool_state = self.address_provider.derive_pool_address(mint) - + # For LetsBonk, vault addresses might need to be derived differently # or provided in the PumpPortal data. For now, we'll derive them # using the standard pattern, but this might need adjustment @@ -91,15 +93,15 @@ class LetsBonkPumpPortalProcessor: quote_vault=None, # Will be filled from additional_accounts ) ) - + # Extract vault addresses if available base_vault = additional_accounts.get("base_vault") quote_vault = additional_accounts.get("quote_vault") - - # If vaults aren't available from additional_accounts, + + # If vaults aren't available from additional_accounts, # we might need to derive them or leave them None # and let the trading logic handle the derivation - + return TokenInfo( name=name, symbol=symbol, @@ -115,4 +117,4 @@ class LetsBonkPumpPortalProcessor: except Exception: logger.exception("Failed to process PumpPortal LetsBonk token data") - return None \ No newline at end of file + return None diff --git a/src/platforms/pumpfun/__init__.py b/src/platforms/pumpfun/__init__.py index fffc606..7ac5565 100644 --- a/src/platforms/pumpfun/__init__.py +++ b/src/platforms/pumpfun/__init__.py @@ -13,9 +13,9 @@ from .pumpportal_processor import PumpFunPumpPortalProcessor # Export implementations for direct use if needed __all__ = [ - 'PumpFunAddressProvider', - 'PumpFunCurveManager', - 'PumpFunEventParser', - 'PumpFunInstructionBuilder', - 'PumpFunPumpPortalProcessor' -] \ No newline at end of file + "PumpFunAddressProvider", + "PumpFunCurveManager", + "PumpFunEventParser", + "PumpFunInstructionBuilder", + "PumpFunPumpPortalProcessor", +] diff --git a/src/platforms/pumpfun/address_provider.py b/src/platforms/pumpfun/address_provider.py index 4f6d4ba..c17e522 100644 --- a/src/platforms/pumpfun/address_provider.py +++ b/src/platforms/pumpfun/address_provider.py @@ -39,7 +39,7 @@ class PumpFunAddresses: def find_global_volume_accumulator() -> Pubkey: """ Derive the Program Derived Address (PDA) for the global volume accumulator. - + Returns: Pubkey of the derived global volume accumulator account """ @@ -53,10 +53,10 @@ class PumpFunAddresses: def find_user_volume_accumulator(user: Pubkey) -> Pubkey: """ Derive the Program Derived Address (PDA) for a user's volume accumulator. - + Args: user: Pubkey of the user account - + Returns: Pubkey of the derived user volume accumulator account """ @@ -69,26 +69,26 @@ class PumpFunAddresses: class PumpFunAddressProvider(AddressProvider): """Pump.Fun implementation of AddressProvider interface.""" - + @property def platform(self) -> Platform: """Get the platform this provider serves.""" return Platform.PUMP_FUN - + @property def program_id(self) -> Pubkey: """Get the main program ID for this platform.""" return PumpFunAddresses.PROGRAM - + def get_system_addresses(self) -> dict[str, Pubkey]: """Get all system addresses required for pump.fun. - + Returns: Dictionary mapping address names to Pubkey objects """ # Get system addresses from the single source of truth system_addresses = SystemAddresses.get_all_system_addresses() - + # Add pump.fun specific addresses pumpfun_addresses = { # Pump.fun specific addresses @@ -98,82 +98,85 @@ class PumpFunAddressProvider(AddressProvider): "fee": PumpFunAddresses.FEE, "liquidity_migrator": PumpFunAddresses.LIQUIDITY_MIGRATOR, } - + # Combine system and platform-specific addresses return {**system_addresses, **pumpfun_addresses} - - def derive_pool_address(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey: + + def derive_pool_address( + self, base_mint: Pubkey, quote_mint: Pubkey | None = None + ) -> Pubkey: """Derive the bonding curve address for a token. - + For pump.fun, this is the bonding curve PDA derived from the mint. - + Args: base_mint: Token mint address quote_mint: Not used for pump.fun (SOL is always the quote) - + Returns: Bonding curve address """ bonding_curve, _ = Pubkey.find_program_address( - [b"bonding-curve", bytes(base_mint)], - PumpFunAddresses.PROGRAM + [b"bonding-curve", bytes(base_mint)], PumpFunAddresses.PROGRAM ) return bonding_curve - + def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey: """Derive user's associated token account address. - + Args: user: User's wallet address mint: Token mint address - + Returns: User's associated token account address """ return get_associated_token_address(user, mint) - + def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]: """Get pump.fun-specific additional accounts needed for trading. - + Args: token_info: Token information - + Returns: Dictionary of additional account addresses """ accounts = {} - + # Add bonding curve if available if token_info.bonding_curve: accounts["bonding_curve"] = token_info.bonding_curve - + # Add associated bonding curve if available if token_info.associated_bonding_curve: accounts["associated_bonding_curve"] = token_info.associated_bonding_curve - + # Add creator vault if available if token_info.creator_vault: accounts["creator_vault"] = token_info.creator_vault - + # Derive associated bonding curve if not provided if not token_info.associated_bonding_curve and token_info.bonding_curve: accounts["associated_bonding_curve"] = self.derive_associated_bonding_curve( token_info.mint, token_info.bonding_curve ) - + # Derive creator vault if not provided but creator is available if not token_info.creator_vault and token_info.creator: accounts["creator_vault"] = self.derive_creator_vault(token_info.creator) - + return accounts - - def derive_associated_bonding_curve(self, mint: Pubkey, bonding_curve: Pubkey) -> Pubkey: + + def derive_associated_bonding_curve( + self, mint: Pubkey, bonding_curve: Pubkey + ) -> Pubkey: """Derive the associated bonding curve (ATA of bonding curve for the token). - + Args: mint: Token mint address bonding_curve: Bonding curve address - + Returns: Associated bonding curve address """ @@ -186,93 +189,108 @@ class PumpFunAddressProvider(AddressProvider): SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, ) return derived_address - + def derive_creator_vault(self, creator: Pubkey) -> Pubkey: """Derive the creator vault address. - + Args: creator: Creator address - + Returns: Creator vault address """ creator_vault, _ = Pubkey.find_program_address( - [b"creator-vault", bytes(creator)], - PumpFunAddresses.PROGRAM + [b"creator-vault", bytes(creator)], PumpFunAddresses.PROGRAM ) return creator_vault - + def derive_global_volume_accumulator(self) -> Pubkey: """Derive the global volume accumulator PDA. - + Returns: Global volume accumulator address """ return PumpFunAddresses.find_global_volume_accumulator() - + def derive_user_volume_accumulator(self, user: Pubkey) -> Pubkey: """Derive the user volume accumulator PDA. - + Args: user: User address - + Returns: User volume accumulator address """ return PumpFunAddresses.find_user_volume_accumulator(user) - - def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]: + + def get_buy_instruction_accounts( + self, token_info: TokenInfo, user: Pubkey + ) -> dict[str, Pubkey]: """Get all accounts needed for a buy instruction. - + Args: token_info: Token information user: User's wallet address - + Returns: Dictionary of account addresses for buy instruction """ additional_accounts = self.get_additional_accounts(token_info) - + return { "global": PumpFunAddresses.GLOBAL, "fee": PumpFunAddresses.FEE, "mint": token_info.mint, - "bonding_curve": additional_accounts.get("bonding_curve", token_info.bonding_curve), - "associated_bonding_curve": additional_accounts.get("associated_bonding_curve", token_info.associated_bonding_curve), + "bonding_curve": additional_accounts.get( + "bonding_curve", token_info.bonding_curve + ), + "associated_bonding_curve": additional_accounts.get( + "associated_bonding_curve", token_info.associated_bonding_curve + ), "user_token_account": self.derive_user_token_account(user, token_info.mint), "user": user, "system_program": SystemAddresses.SYSTEM_PROGRAM, "token_program": SystemAddresses.TOKEN_PROGRAM, - "creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault), + "creator_vault": additional_accounts.get( + "creator_vault", token_info.creator_vault + ), "event_authority": PumpFunAddresses.EVENT_AUTHORITY, "program": PumpFunAddresses.PROGRAM, "global_volume_accumulator": self.derive_global_volume_accumulator(), "user_volume_accumulator": self.derive_user_volume_accumulator(user), } - - def get_sell_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]: + + def get_sell_instruction_accounts( + self, token_info: TokenInfo, user: Pubkey + ) -> dict[str, Pubkey]: """Get all accounts needed for a sell instruction. - + Args: token_info: Token information user: User's wallet address - + Returns: Dictionary of account addresses for sell instruction """ additional_accounts = self.get_additional_accounts(token_info) - + return { "global": PumpFunAddresses.GLOBAL, "fee": PumpFunAddresses.FEE, "mint": token_info.mint, - "bonding_curve": additional_accounts.get("bonding_curve", token_info.bonding_curve), - "associated_bonding_curve": additional_accounts.get("associated_bonding_curve", token_info.associated_bonding_curve), + "bonding_curve": additional_accounts.get( + "bonding_curve", token_info.bonding_curve + ), + "associated_bonding_curve": additional_accounts.get( + "associated_bonding_curve", token_info.associated_bonding_curve + ), "user_token_account": self.derive_user_token_account(user, token_info.mint), "user": user, "system_program": SystemAddresses.SYSTEM_PROGRAM, - "creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault), + "creator_vault": additional_accounts.get( + "creator_vault", token_info.creator_vault + ), "token_program": SystemAddresses.TOKEN_PROGRAM, "event_authority": PumpFunAddresses.EVENT_AUTHORITY, "program": PumpFunAddresses.PROGRAM, - } \ No newline at end of file + } diff --git a/src/platforms/pumpfun/curve_manager.py b/src/platforms/pumpfun/curve_manager.py index b4948e6..7d98011 100644 --- a/src/platforms/pumpfun/curve_manager.py +++ b/src/platforms/pumpfun/curve_manager.py @@ -20,30 +20,30 @@ logger = get_logger(__name__) class PumpFunCurveManager(CurveManager): """Pump.Fun implementation of CurveManager interface using IDL-based decoding.""" - + def __init__(self, client: SolanaClient, idl_parser: IDLParser): """Initialize pump.fun curve manager with injected IDL parser. - + Args: client: Solana RPC client idl_parser: Pre-loaded IDL parser for pump.fun platform """ self.client = client self._idl_parser = idl_parser - + logger.info("Pump.Fun curve manager initialized with injected IDL parser") - + @property def platform(self) -> Platform: """Get the platform this manager serves.""" return Platform.PUMP_FUN - + async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]: """Get the current state of a pump.fun bonding curve. - + Args: pool_address: Address of the bonding curve - + Returns: Dictionary containing bonding curve state data """ @@ -51,140 +51,139 @@ class PumpFunCurveManager(CurveManager): account = await self.client.get_account_info(pool_address) if not account.data: raise ValueError(f"No data in bonding curve account {pool_address}") - + # Decode bonding curve state using injected IDL parser curve_state_data = self._decode_curve_state_with_idl(account.data) - + return curve_state_data - + except Exception as e: logger.exception("Failed to get curve state") raise ValueError(f"Invalid bonding curve state: {e!s}") - + async def calculate_price(self, pool_address: Pubkey) -> float: """Calculate current token price from bonding curve state. - + Args: pool_address: Address of the bonding curve - + Returns: Current token price in SOL """ pool_state = await self.get_pool_state(pool_address) - + # Use virtual reserves for price calculation virtual_token_reserves = pool_state["virtual_token_reserves"] virtual_sol_reserves = pool_state["virtual_sol_reserves"] - + if virtual_token_reserves <= 0: return 0.0 - + # Price = sol_reserves / token_reserves price_lamports = virtual_sol_reserves / virtual_token_reserves return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL - + async def calculate_buy_amount_out( - self, - pool_address: Pubkey, - amount_in: int + self, pool_address: Pubkey, amount_in: int ) -> int: """Calculate expected tokens received for a buy operation. - + Uses the pump.fun bonding curve formula to calculate token output. - + Args: pool_address: Address of the bonding curve amount_in: Amount of SOL to spend (in lamports) - + Returns: Expected amount of tokens to receive (in raw token units) """ pool_state = await self.get_pool_state(pool_address) - + virtual_token_reserves = pool_state["virtual_token_reserves"] virtual_sol_reserves = pool_state["virtual_sol_reserves"] - + # Use virtual reserves for bonding curve calculation # Formula: tokens_out = (amount_in * virtual_token_reserves) / (virtual_sol_reserves + amount_in) numerator = amount_in * virtual_token_reserves denominator = virtual_sol_reserves + amount_in - + if denominator == 0: return 0 - + tokens_out = numerator // denominator return tokens_out - + async def calculate_sell_amount_out( - self, - pool_address: Pubkey, - amount_in: int + self, pool_address: Pubkey, amount_in: int ) -> int: """Calculate expected SOL received for a sell operation. - + Uses the pump.fun bonding curve formula to calculate SOL output. - + Args: pool_address: Address of the bonding curve amount_in: Amount of tokens to sell (in raw token units) - + Returns: Expected amount of SOL to receive (in lamports) """ pool_state = await self.get_pool_state(pool_address) - + virtual_token_reserves = pool_state["virtual_token_reserves"] virtual_sol_reserves = pool_state["virtual_sol_reserves"] - + # Use virtual reserves for bonding curve calculation # Formula: sol_out = (amount_in * virtual_sol_reserves) / (virtual_token_reserves + amount_in) numerator = amount_in * virtual_sol_reserves denominator = virtual_token_reserves + amount_in - + if denominator == 0: return 0 - + sol_out = numerator // denominator return sol_out - + async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]: """Get current bonding curve reserves. - + Args: pool_address: Address of the bonding curve - + Returns: Tuple of (token_reserves, sol_reserves) in raw units """ pool_state = await self.get_pool_state(pool_address) - return (pool_state["virtual_token_reserves"], pool_state["virtual_sol_reserves"]) - + return ( + pool_state["virtual_token_reserves"], + pool_state["virtual_sol_reserves"], + ) + def _decode_curve_state_with_idl(self, data: bytes) -> dict[str, Any]: """Decode bonding curve state data using injected IDL parser. - + Args: data: Raw account data - + Returns: Dictionary with decoded bonding curve state - + Raises: ValueError: If IDL parsing fails """ # Use injected IDL parser to decode BondingCurve account data decoded_curve_state = self._idl_parser.decode_account_data( - data, - "BondingCurve", - skip_discriminator=True + data, "BondingCurve", skip_discriminator=True ) - + if not decoded_curve_state: raise ValueError("Failed to decode bonding curve state with IDL parser") - + # Extract the fields we need for trading calculations # Based on the BondingCurve structure from the IDL curve_data = { - "virtual_token_reserves": decoded_curve_state.get("virtual_token_reserves", 0), + "virtual_token_reserves": decoded_curve_state.get( + "virtual_token_reserves", 0 + ), "virtual_sol_reserves": decoded_curve_state.get("virtual_sol_reserves", 0), "real_token_reserves": decoded_curve_state.get("real_token_reserves", 0), "real_sol_reserves": decoded_curve_state.get("real_sol_reserves", 0), @@ -192,106 +191,121 @@ class PumpFunCurveManager(CurveManager): "complete": decoded_curve_state.get("complete", False), "creator": decoded_curve_state.get("creator", ""), } - + # Calculate additional metrics if curve_data["virtual_token_reserves"] > 0: curve_data["price_per_token"] = ( - (curve_data["virtual_sol_reserves"] / curve_data["virtual_token_reserves"]) - * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL + ( + curve_data["virtual_sol_reserves"] + / curve_data["virtual_token_reserves"] + ) + * (10**TOKEN_DECIMALS) + / LAMPORTS_PER_SOL ) else: curve_data["price_per_token"] = 0 - + # Add convenience decimal fields - curve_data["token_reserves_decimal"] = curve_data["virtual_token_reserves"] / 10**TOKEN_DECIMALS - curve_data["sol_reserves_decimal"] = curve_data["virtual_sol_reserves"] / LAMPORTS_PER_SOL - - logger.debug(f"Decoded curve state: virtual_token_reserves={curve_data['virtual_token_reserves']}, " - f"virtual_sol_reserves={curve_data['virtual_sol_reserves']}, " - f"price={curve_data['price_per_token']:.8f} SOL") - + curve_data["token_reserves_decimal"] = ( + curve_data["virtual_token_reserves"] / 10**TOKEN_DECIMALS + ) + curve_data["sol_reserves_decimal"] = ( + curve_data["virtual_sol_reserves"] / LAMPORTS_PER_SOL + ) + + logger.debug( + f"Decoded curve state: virtual_token_reserves={curve_data['virtual_token_reserves']}, " + f"virtual_sol_reserves={curve_data['virtual_sol_reserves']}, " + f"price={curve_data['price_per_token']:.8f} SOL" + ) + return curve_data - + # Additional convenience methods for pump.fun specific operations - async def calculate_expected_tokens(self, pool_address: Pubkey, sol_amount: float) -> float: + async def calculate_expected_tokens( + self, pool_address: Pubkey, sol_amount: float + ) -> float: """Calculate the expected token amount for a given SOL input. - + This is a convenience method that converts between decimal and raw units. - + Args: pool_address: Address of the bonding curve sol_amount: Amount of SOL to spend (in decimal SOL) - + Returns: Expected token amount (in decimal tokens) """ sol_lamports = int(sol_amount * LAMPORTS_PER_SOL) tokens_raw = await self.calculate_buy_amount_out(pool_address, sol_lamports) return tokens_raw / 10**TOKEN_DECIMALS - - async def calculate_expected_sol(self, pool_address: Pubkey, token_amount: float) -> float: + + async def calculate_expected_sol( + self, pool_address: Pubkey, token_amount: float + ) -> float: """Calculate the expected SOL amount for a given token input. - + This is a convenience method that converts between decimal and raw units. - + Args: pool_address: Address of the bonding curve token_amount: Amount of tokens to sell (in decimal tokens) - + Returns: Expected SOL amount (in decimal SOL) """ tokens_raw = int(token_amount * 10**TOKEN_DECIMALS) sol_lamports = await self.calculate_sell_amount_out(pool_address, tokens_raw) return sol_lamports / LAMPORTS_PER_SOL - + async def is_curve_complete(self, pool_address: Pubkey) -> bool: """Check if the bonding curve is complete (migrated to Raydium). - + Args: pool_address: Address of the bonding curve - + Returns: True if curve is complete, False otherwise """ pool_state = await self.get_pool_state(pool_address) return pool_state.get("complete", False) - + async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]: """Get bonding curve completion progress information. - + Args: pool_address: Address of the bonding curve - + Returns: Dictionary with progress information """ pool_state = await self.get_pool_state(pool_address) - + # Calculate progress based on SOL raised vs target # This is approximate since the exact target isn't stored in the curve state sol_raised = pool_state["real_sol_reserves"] / LAMPORTS_PER_SOL - + # Estimate progress based on typical pump.fun graduation requirements # (This could be made more accurate with additional on-chain data) estimated_target_sol = 85.0 # Typical pump.fun graduation target progress_percentage = min((sol_raised / estimated_target_sol) * 100, 100.0) - + return { "complete": pool_state.get("complete", False), "sol_raised": sol_raised, "estimated_target_sol": estimated_target_sol, "progress_percentage": progress_percentage, - "tokens_available": pool_state["virtual_token_reserves"] / 10**TOKEN_DECIMALS, + "tokens_available": pool_state["virtual_token_reserves"] + / 10**TOKEN_DECIMALS, "market_cap_sol": sol_raised, # Approximate market cap } - + def validate_curve_state_structure(self, pool_address: Pubkey) -> bool: """Validate that the curve state structure matches IDL expectations. - + Args: pool_address: Address of the bonding curve - + Returns: True if structure is valid, False otherwise """ @@ -299,24 +313,29 @@ class PumpFunCurveManager(CurveManager): # This would be used during development/testing to ensure # the IDL parsing is working correctly pool_state = self.get_pool_state(pool_address) - + required_fields = [ - "virtual_token_reserves", "virtual_sol_reserves", - "real_token_reserves", "real_sol_reserves", - "token_total_supply", "complete" + "virtual_token_reserves", + "virtual_sol_reserves", + "real_token_reserves", + "real_sol_reserves", + "token_total_supply", + "complete", ] - + for field in required_fields: if field not in pool_state: logger.error(f"Missing required field: {field}") return False - + if field != "complete" and not isinstance(pool_state[field], int): - logger.error(f"Field {field} is not an integer: {type(pool_state[field])}") + logger.error( + f"Field {field} is not an integer: {type(pool_state[field])}" + ) return False - + return True - + except Exception: logger.exception("Curve state validation failed") - return False \ No newline at end of file + return False diff --git a/src/platforms/pumpfun/event_parser.py b/src/platforms/pumpfun/event_parser.py index 3ff620d..d12b3b7 100644 --- a/src/platforms/pumpfun/event_parser.py +++ b/src/platforms/pumpfun/event_parser.py @@ -27,40 +27,50 @@ class PumpFunEventParser(EventParser): def __init__(self, idl_parser: IDLParser): """Initialize pump.fun event parser with injected IDL parser. - + Args: idl_parser: Pre-loaded IDL parser for pump.fun platform """ self._idl_parser = idl_parser - + event_discriminators = self._idl_parser.get_event_discriminators() self._create_event_discriminator_bytes = event_discriminators["CreateEvent"] - self._create_event_discriminator = struct.unpack(" Platform: """Get the platform this parser serves.""" return Platform.PUMP_FUN - + def parse_token_creation_from_logs( - self, - logs: list[str], - signature: str + self, logs: list[str], signature: str ) -> TokenInfo | None: """Parse token creation from pump.fun transaction logs using IDL event parsing. - + Args: logs: List of log strings from transaction signature: Transaction signature - + Returns: TokenInfo if token creation found, None otherwise """ @@ -73,13 +83,13 @@ class PumpFunEventParser(EventParser): return None logger.info(f"🔍 Parsing token creation from logs for signature: {signature}") - + # Look for event data in the logs (CreateEvent data!) # We need to find the Program data that comes after "Instruction: Create" try: create_instruction_found = False program_data_entries = [] - + # First, collect all Program data entries and note when Create instruction happens for i, log in enumerate(logs): if "Program log: Instruction: Create" in log: @@ -89,87 +99,141 @@ class PumpFunEventParser(EventParser): # Extract base64 encoded event data encoded_data = log.split("Program data: ")[1].strip() program_data_entries.append((i, encoded_data, log)) - logger.info(f"📊 Found Program data at log index {i}, length: {len(encoded_data)}") - + logger.info( + f"📊 Found Program data at log index {i}, length: {len(encoded_data)}" + ) + if not create_instruction_found: logger.info("❌ No Create instruction found in logs") return None - + if not program_data_entries: logger.info("❌ No Program data entries found in logs") return None - - logger.info(f"🔍 Found {len(program_data_entries)} Program data entries to check") - + + logger.info( + f"🔍 Found {len(program_data_entries)} Program data entries to check" + ) + # Try each Program data entry - for entry_idx, (log_idx, encoded_data, full_log) in enumerate(program_data_entries): + for entry_idx, (log_idx, encoded_data, full_log) in enumerate( + program_data_entries + ): try: - logger.info(f"🧪 Trying Program data entry {entry_idx + 1}/{len(program_data_entries)} (log index {log_idx})") - + logger.info( + f"🧪 Trying Program data entry {entry_idx + 1}/{len(program_data_entries)} (log index {log_idx})" + ) + decoded_data = base64.b64decode(encoded_data) - + if len(decoded_data) < 8: - logger.info(f"⚠️ Program data too short: {len(decoded_data)} bytes") + logger.info( + f"⚠️ Program data too short: {len(decoded_data)} bytes" + ) continue - + # Check discriminator from program data discriminator = decoded_data[:8] discriminator_int = struct.unpack(" TokenInfo | None: """Parse token creation from pump.fun instruction data using injected IDL parser. - + Args: instruction_data: Raw instruction data accounts: List of account indices account_keys: List of account public keys - + Returns: TokenInfo if token creation found, None otherwise """ - if not instruction_data.startswith(self._create_instruction_discriminator_bytes): + if not instruction_data.startswith( + self._create_instruction_discriminator_bytes + ): return None try: @@ -225,12 +290,14 @@ class PumpFunEventParser(EventParser): return Pubkey.from_bytes(account_keys[account_index]) # Parse instruction data using injected IDL parser - decoded = self._idl_parser.decode_instruction(instruction_data, account_keys, accounts) - if not decoded or decoded['instruction_name'] != 'create': + decoded = self._idl_parser.decode_instruction( + instruction_data, account_keys, accounts + ) + if not decoded or decoded["instruction_name"] != "create": return None - - args = decoded.get('args', {}) - + + args = decoded.get("args", {}) + # Extract account information based on IDL account order mint = get_account_key(0) bonding_curve = get_account_key(2) @@ -241,7 +308,11 @@ class PumpFunEventParser(EventParser): return None # Create creator vault - creator = Pubkey.from_string(args.get("creator", str(user))) if args.get("creator") else user + creator = ( + Pubkey.from_string(args.get("creator", str(user))) + if args.get("creator") + else user + ) creator_vault = self._derive_creator_vault(creator) return TokenInfo( @@ -261,21 +332,20 @@ class PumpFunEventParser(EventParser): except Exception as e: logger.debug(f"Failed to parse create instruction: {e}") return None - + def parse_token_creation_from_geyser( - self, - transaction_info: Any + self, transaction_info: Any ) -> TokenInfo | None: """Parse token creation from Geyser transaction data. - + Args: transaction_info: Geyser transaction information - + Returns: TokenInfo if token creation found, None otherwise """ try: - if not hasattr(transaction_info, 'transaction'): + if not hasattr(transaction_info, "transaction"): return None tx = transaction_info.transaction.transaction.transaction @@ -304,37 +374,37 @@ class PumpFunEventParser(EventParser): except Exception as e: logger.debug(f"Failed to parse geyser transaction: {e}") return None - + def get_program_id(self) -> Pubkey: """Get the pump.fun program ID this parser monitors. - + Returns: Pump.fun program ID """ return PumpFunAddresses.PROGRAM - + def get_instruction_discriminators(self) -> list[bytes]: """Get instruction discriminators for token creation. - + Returns: List of discriminator bytes to match """ return [self._create_instruction_discriminator_bytes] - + def get_event_discriminators(self) -> list[bytes]: """Get event discriminators for token creation. - + Returns: List of event discriminator bytes to match """ return [self._create_event_discriminator_bytes] - + def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None: """Parse token creation from block data (for block listener). - + Args: block_data: Block data from WebSocket - + Returns: TokenInfo if token creation found, None otherwise """ @@ -353,21 +423,26 @@ class PumpFunEventParser(EventParser): tx_data_encoded = tx_data[0] tx_data_decoded = base64.b64decode(tx_data_encoded) transaction = VersionedTransaction.from_bytes(tx_data_decoded) - + for ix in transaction.message.instructions: - program_id = transaction.message.account_keys[ix.program_id_index] - + program_id = transaction.message.account_keys[ + ix.program_id_index + ] + # Check if instruction is from pump.fun program if str(program_id) != str(self.get_program_id()): continue ix_data = bytes(ix.data) - + # Check for create discriminator if len(ix_data) >= 8: discriminator = struct.unpack("= len(message["accountKeys"]): continue - + program_id_str = message["accountKeys"][program_idx] if program_id_str != str(self.get_program_id()): continue - + # Decode instruction data ix_data = base64.b64decode(ix["data"]) - + if len(ix_data) >= 8: discriminator = struct.unpack(" Pubkey: """Derive the creator vault for a creator. - + Args: creator: Creator address - + Returns: Creator vault address """ @@ -453,14 +542,16 @@ class PumpFunEventParser(EventParser): PumpFunAddresses.PROGRAM, ) return derived_address - - def _derive_associated_bonding_curve(self, mint: Pubkey, bonding_curve: Pubkey) -> Pubkey: + + def _derive_associated_bonding_curve( + self, mint: Pubkey, bonding_curve: Pubkey + ) -> Pubkey: """Derive the associated bonding curve (ATA of bonding curve for the token). - + Args: mint: Token mint address bonding_curve: Bonding curve address - + Returns: Associated bonding curve address """ @@ -474,12 +565,12 @@ class PumpFunEventParser(EventParser): ) return derived_address - @property + @property def verbose(self) -> bool: """Check if verbose logging is enabled.""" - return getattr(self, '_verbose', False) - + return getattr(self, "_verbose", False) + @verbose.setter def verbose(self, value: bool) -> None: """Set verbose logging.""" - self._verbose = value \ No newline at end of file + self._verbose = value diff --git a/src/platforms/pumpfun/instruction_builder.py b/src/platforms/pumpfun/instruction_builder.py index 474dfdc..4540c5c 100644 --- a/src/platforms/pumpfun/instruction_builder.py +++ b/src/platforms/pumpfun/instruction_builder.py @@ -21,52 +21,52 @@ logger = get_logger(__name__) class PumpFunInstructionBuilder(InstructionBuilder): """Pump.Fun implementation of InstructionBuilder interface with IDL-based discriminators.""" - + def __init__(self, idl_parser: IDLParser): """Initialize pump.fun instruction builder with injected IDL parser. - + Args: idl_parser: Pre-loaded IDL parser for pump.fun platform """ self._idl_parser = idl_parser - + # Get discriminators from injected IDL parser discriminators = self._idl_parser.get_instruction_discriminators() self._buy_discriminator = discriminators["buy"] self._sell_discriminator = discriminators["sell"] - + logger.info("Pump.Fun instruction builder initialized with injected IDL parser") - + @property def platform(self) -> Platform: """Get the platform this builder serves.""" return Platform.PUMP_FUN - + async def build_buy_instruction( self, token_info: TokenInfo, user: Pubkey, amount_in: int, minimum_amount_out: int, - address_provider: AddressProvider + address_provider: AddressProvider, ) -> list[Instruction]: """Build buy instruction(s) for pump.fun. - + Args: token_info: Token information user: User's wallet address amount_in: Amount of SOL to spend (in lamports) minimum_amount_out: Minimum tokens expected (raw token units) address_provider: Platform address provider - + Returns: List of instructions needed for the buy operation """ instructions = [] - + # Get all required accounts accounts_info = address_provider.get_buy_instruction_accounts(token_info, user) - + # 1. Create idempotent ATA instruction (won't fail if ATA already exists) ata_instruction = create_idempotent_associated_token_account( user, # payer @@ -75,116 +75,181 @@ class PumpFunInstructionBuilder(InstructionBuilder): SystemAddresses.TOKEN_PROGRAM, # token program ) instructions.append(ata_instruction) - + # 2. Build buy instruction buy_accounts = [ - AccountMeta(pubkey=accounts_info["global"], is_signer=False, is_writable=False), + AccountMeta( + pubkey=accounts_info["global"], is_signer=False, is_writable=False + ), AccountMeta(pubkey=accounts_info["fee"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["mint"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["associated_bonding_curve"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["user_token_account"], is_signer=False, is_writable=True), + AccountMeta( + pubkey=accounts_info["mint"], is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=accounts_info["associated_bonding_curve"], + is_signer=False, + is_writable=True, + ), + AccountMeta( + pubkey=accounts_info["user_token_account"], + is_signer=False, + is_writable=True, + ), AccountMeta(pubkey=accounts_info["user"], is_signer=True, is_writable=True), - AccountMeta(pubkey=accounts_info["system_program"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["token_program"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["global_volume_accumulator"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["user_volume_accumulator"], is_signer=False, is_writable=True), + AccountMeta( + pubkey=accounts_info["system_program"], + is_signer=False, + is_writable=False, + ), + AccountMeta( + pubkey=accounts_info["token_program"], + is_signer=False, + is_writable=False, + ), + AccountMeta( + pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=accounts_info["event_authority"], + is_signer=False, + is_writable=False, + ), + AccountMeta( + pubkey=accounts_info["program"], is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=accounts_info["global_volume_accumulator"], + is_signer=False, + is_writable=True, + ), + AccountMeta( + pubkey=accounts_info["user_volume_accumulator"], + is_signer=False, + is_writable=True, + ), ] - + # Build instruction data: discriminator + token_amount + max_sol_cost instruction_data = ( - self._buy_discriminator + - struct.pack(" list[Instruction]: """Build sell instruction(s) for pump.fun. - + Args: token_info: Token information user: User's wallet address amount_in: Amount of tokens to sell (raw token units) minimum_amount_out: Minimum SOL expected (in lamports) address_provider: Platform address provider - + Returns: List of instructions needed for the sell operation """ instructions = [] - + # Get all required accounts accounts_info = address_provider.get_sell_instruction_accounts(token_info, user) - + # Build sell instruction accounts sell_accounts = [ - AccountMeta(pubkey=accounts_info["global"], is_signer=False, is_writable=False), + AccountMeta( + pubkey=accounts_info["global"], is_signer=False, is_writable=False + ), AccountMeta(pubkey=accounts_info["fee"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["mint"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["associated_bonding_curve"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["user_token_account"], is_signer=False, is_writable=True), + AccountMeta( + pubkey=accounts_info["mint"], is_signer=False, is_writable=False + ), + AccountMeta( + pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=accounts_info["associated_bonding_curve"], + is_signer=False, + is_writable=True, + ), + AccountMeta( + pubkey=accounts_info["user_token_account"], + is_signer=False, + is_writable=True, + ), AccountMeta(pubkey=accounts_info["user"], is_signer=True, is_writable=True), - AccountMeta(pubkey=accounts_info["system_program"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["token_program"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), + AccountMeta( + pubkey=accounts_info["system_program"], + is_signer=False, + is_writable=False, + ), + AccountMeta( + pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True + ), + AccountMeta( + pubkey=accounts_info["token_program"], + is_signer=False, + is_writable=False, + ), + AccountMeta( + pubkey=accounts_info["event_authority"], + is_signer=False, + is_writable=False, + ), + AccountMeta( + pubkey=accounts_info["program"], is_signer=False, is_writable=False + ), ] - + # Build instruction data: discriminator + token_amount + min_sol_output instruction_data = ( - self._sell_discriminator + - struct.pack(" list[Pubkey]: """Get list of accounts required for buy operation (for priority fee calculation). - + Args: token_info: Token information user: User's wallet address address_provider: Platform address provider - + Returns: List of account addresses that will be accessed """ accounts_info = address_provider.get_buy_instruction_accounts(token_info, user) - + return [ accounts_info["mint"], accounts_info["bonding_curve"], @@ -196,25 +261,22 @@ class PumpFunInstructionBuilder(InstructionBuilder): accounts_info["user_volume_accumulator"], accounts_info["program"], ] - + def get_required_accounts_for_sell( - self, - token_info: TokenInfo, - user: Pubkey, - address_provider: AddressProvider + self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider ) -> list[Pubkey]: """Get list of accounts required for sell operation (for priority fee calculation). - + Args: token_info: Token information user: User's wallet address address_provider: Platform address provider - + Returns: List of account addresses that will be accessed """ accounts_info = address_provider.get_sell_instruction_accounts(token_info, user) - + return [ accounts_info["mint"], accounts_info["bonding_curve"], @@ -224,25 +286,25 @@ class PumpFunInstructionBuilder(InstructionBuilder): accounts_info["creator_vault"], accounts_info["program"], ] - + def calculate_token_amount_raw(self, token_amount_decimal: float) -> int: """Convert decimal token amount to raw token units. - + Args: token_amount_decimal: Token amount in decimal form - + Returns: Token amount in raw units (adjusted for decimals) """ return int(token_amount_decimal * 10**TOKEN_DECIMALS) - + def calculate_token_amount_decimal(self, token_amount_raw: int) -> float: """Convert raw token amount to decimal form. - + Args: token_amount_raw: Token amount in raw units - + Returns: Token amount in decimal form """ - return token_amount_raw / 10**TOKEN_DECIMALS \ No newline at end of file + return token_amount_raw / 10**TOKEN_DECIMALS diff --git a/src/platforms/pumpfun/pumpportal_processor.py b/src/platforms/pumpfun/pumpportal_processor.py index 1c56123..6ab1bf9 100644 --- a/src/platforms/pumpfun/pumpportal_processor.py +++ b/src/platforms/pumpfun/pumpportal_processor.py @@ -14,39 +14,39 @@ logger = get_logger(__name__) class PumpFunPumpPortalProcessor: """PumpPortal processor for pump.fun tokens.""" - + def __init__(self): """Initialize the processor with address provider.""" self.address_provider = PumpFunAddressProvider() - + @property def platform(self) -> Platform: """Get the platform this processor handles.""" return Platform.PUMP_FUN - + @property def supported_pool_names(self) -> list[str]: """Get the pool names this processor supports from PumpPortal.""" return ["pump"] # PumpPortal pool name for pump.fun - + def can_process(self, token_data: dict) -> bool: """Check if this processor can handle the given token data. - + Args: token_data: Token data from PumpPortal - + Returns: True if this processor can handle the token data """ pool = token_data.get("pool", "").lower() return pool in self.supported_pool_names - + def process_token_data(self, token_data: dict) -> TokenInfo | None: """Process pump.fun token data from PumpPortal. - + Args: token_data: Token data from PumpPortal WebSocket - + Returns: TokenInfo if token creation found, None otherwise """ @@ -81,8 +81,10 @@ class PumpFunPumpPortalProcessor: creator = user # Derive additional addresses using platform provider - associated_bonding_curve = self.address_provider.derive_associated_bonding_curve( - mint, bonding_curve + associated_bonding_curve = ( + self.address_provider.derive_associated_bonding_curve( + mint, bonding_curve + ) ) creator_vault = self.address_provider.derive_creator_vault(creator) @@ -101,4 +103,4 @@ class PumpFunPumpPortalProcessor: except Exception: logger.exception("Failed to process PumpPortal token data") - return None \ No newline at end of file + return None diff --git a/src/trading/base.py b/src/trading/base.py index e2cb83d..c04bef4 100644 --- a/src/trading/base.py +++ b/src/trading/base.py @@ -19,6 +19,7 @@ from interfaces.core import Platform, TokenInfo @dataclass class TradeResult: """Enhanced result of a trading operation with platform support.""" + success: bool platform: Platform = Platform.PUMP_FUN # Add platform tracking tx_signature: str | None = None @@ -28,7 +29,7 @@ class TradeResult: def to_dict(self) -> dict[str, Any]: """Convert to dictionary for logging/serialization. - + Returns: Dictionary representation of the trade result """ @@ -51,7 +52,7 @@ class Trader(ABC): Args: token_info: Enhanced token information with platform support - + Returns: TradeResult with operation outcome including platform info """ @@ -60,7 +61,7 @@ class Trader(ABC): def _get_relevant_accounts(self, token_info: TokenInfo) -> list[Pubkey]: """ Get the list of accounts relevant for calculating the priority fee. - + This is now platform-agnostic and should be overridden by platform-specific traders. Args: @@ -71,13 +72,13 @@ class Trader(ABC): """ # Basic implementation - platform-specific traders should override this accounts = [token_info.mint] - + if token_info.bonding_curve: accounts.append(token_info.bonding_curve) - + if token_info.pool_state: # For other platforms accounts.append(token_info.pool_state) - + return accounts @@ -85,6 +86,7 @@ class Trader(ABC): @dataclass class TokenInfo_Legacy: """Legacy token information structure for backward compatibility.""" + name: str symbol: str uri: str @@ -138,13 +140,13 @@ class TokenInfo_Legacy: def upgrade_token_info(legacy_token_info: TokenInfo_Legacy) -> TokenInfo: """Convert legacy TokenInfo to enhanced TokenInfo. - + This function allows existing code that creates legacy TokenInfo objects to be upgraded to the new enhanced format. - + Args: legacy_token_info: Legacy TokenInfo instance - + Returns: Enhanced TokenInfo with platform information """ @@ -164,29 +166,31 @@ def upgrade_token_info(legacy_token_info: TokenInfo_Legacy) -> TokenInfo: def create_legacy_token_info(enhanced_token_info: TokenInfo) -> TokenInfo_Legacy: """Convert enhanced TokenInfo back to legacy TokenInfo if needed. - + This function allows the enhanced TokenInfo to be used with existing code that expects the legacy format. - + Args: enhanced_token_info: Enhanced TokenInfo instance - + Returns: Legacy TokenInfo instance - + Raises: ValueError: If enhanced TokenInfo doesn't have required pump.fun fields """ if enhanced_token_info.platform != Platform.PUMP_FUN: raise ValueError("Can only convert pump.fun tokens to legacy format") - - if not all([ - enhanced_token_info.bonding_curve, - enhanced_token_info.associated_bonding_curve, - enhanced_token_info.creator_vault - ]): + + if not all( + [ + enhanced_token_info.bonding_curve, + enhanced_token_info.associated_bonding_curve, + enhanced_token_info.creator_vault, + ] + ): raise ValueError("Enhanced TokenInfo missing required pump.fun fields") - + return TokenInfo_Legacy( name=enhanced_token_info.name, symbol=enhanced_token_info.symbol, @@ -210,10 +214,10 @@ def create_pump_fun_token_info( user: Pubkey, creator: Pubkey | None = None, creator_vault: Pubkey | None = None, - **kwargs + **kwargs, ) -> TokenInfo: """Convenience function to create pump.fun TokenInfo with proper platform setting. - + Args: name: Token name symbol: Token symbol @@ -225,7 +229,7 @@ def create_pump_fun_token_info( creator: Creator address (defaults to user if not provided) creator_vault: Creator vault address (will be derived if not provided) **kwargs: Additional fields for TokenInfo - + Returns: Enhanced TokenInfo configured for pump.fun """ @@ -234,7 +238,7 @@ def create_pump_fun_token_info( # We can't import PumpAddresses here, so this will need to be handled elsewhere # For now, leave it as None and let the platform implementation handle it pass - + return TokenInfo( name=name, symbol=symbol, @@ -246,7 +250,7 @@ def create_pump_fun_token_info( user=user, creator=creator or user, creator_vault=creator_vault, - **kwargs + **kwargs, ) @@ -260,10 +264,10 @@ def create_lets_bonk_token_info( quote_vault: Pubkey, user: Pubkey, creator: Pubkey | None = None, - **kwargs + **kwargs, ) -> TokenInfo: """Convenience function to create LetsBonk TokenInfo with proper platform setting. - + Args: name: Token name symbol: Token symbol @@ -275,7 +279,7 @@ def create_lets_bonk_token_info( user: User/trader address creator: Creator address (defaults to user if not provided) **kwargs: Additional fields for TokenInfo - + Returns: Enhanced TokenInfo configured for LetsBonk """ @@ -290,16 +294,16 @@ def create_lets_bonk_token_info( quote_vault=quote_vault, user=user, creator=creator or user, - **kwargs + **kwargs, ) def is_pump_fun_token(token_info: TokenInfo) -> bool: """Check if a TokenInfo is for pump.fun platform. - + Args: token_info: Token information to check - + Returns: True if token is for pump.fun platform """ @@ -308,10 +312,10 @@ def is_pump_fun_token(token_info: TokenInfo) -> bool: def is_lets_bonk_token(token_info: TokenInfo) -> bool: """Check if a TokenInfo is for LetsBonk platform. - + Args: token_info: Token information to check - + Returns: True if token is for LetsBonk platform """ @@ -320,10 +324,10 @@ def is_lets_bonk_token(token_info: TokenInfo) -> bool: def get_platform_specific_fields(token_info: TokenInfo) -> dict[str, Any]: """Get platform-specific fields from TokenInfo. - + Args: token_info: Token information - + Returns: Dictionary of platform-specific fields """ @@ -345,51 +349,57 @@ def get_platform_specific_fields(token_info: TokenInfo) -> dict[str, Any]: def validate_token_info(token_info: TokenInfo) -> bool: """Validate that TokenInfo has required fields for its platform. - + Args: token_info: Token information to validate - + Returns: True if TokenInfo is valid for its platform """ # Check common required fields - if not all([ - token_info.name, - token_info.symbol, - token_info.mint, - token_info.platform, - ]): + if not all( + [ + token_info.name, + token_info.symbol, + token_info.mint, + token_info.platform, + ] + ): return False - + # Check platform-specific required fields if token_info.platform == Platform.PUMP_FUN: - return all([ - token_info.bonding_curve, - token_info.associated_bonding_curve, - ]) + return all( + [ + token_info.bonding_curve, + token_info.associated_bonding_curve, + ] + ) elif token_info.platform == Platform.LETS_BONK: - return all([ - token_info.pool_state, - token_info.base_vault, - token_info.quote_vault, - ]) - + return all( + [ + token_info.pool_state, + token_info.base_vault, + token_info.quote_vault, + ] + ) + return True # Backward compatibility exports __all__ = [ - 'Platform', # Platform enum - 'TokenInfo', # Enhanced TokenInfo (main export) - 'TokenInfo_Legacy', # Legacy TokenInfo for compatibility - 'TradeResult', # Enhanced TradeResult - 'Trader', # Enhanced Trader base class - 'create_legacy_token_info', - 'create_lets_bonk_token_info', - 'create_pump_fun_token_info', - 'get_platform_specific_fields', - 'is_lets_bonk_token', - 'is_pump_fun_token', - 'upgrade_token_info', - 'validate_token_info', -] \ No newline at end of file + "Platform", # Platform enum + "TokenInfo", # Enhanced TokenInfo (main export) + "TokenInfo_Legacy", # Legacy TokenInfo for compatibility + "TradeResult", # Enhanced TradeResult + "Trader", # Enhanced Trader base class + "create_legacy_token_info", + "create_lets_bonk_token_info", + "create_pump_fun_token_info", + "get_platform_specific_fields", + "is_lets_bonk_token", + "is_pump_fun_token", + "upgrade_token_info", + "validate_token_info", +] diff --git a/src/trading/platform_aware.py b/src/trading/platform_aware.py index 05d63fd..589ffca 100644 --- a/src/trading/platform_aware.py +++ b/src/trading/platform_aware.py @@ -45,7 +45,9 @@ class PlatformAwareBuyer(Trader): """Execute buy operation using platform-specific implementations.""" try: # Get platform-specific implementations - implementations = get_platform_implementations(token_info.platform, self.client) + implementations = get_platform_implementations( + token_info.platform, self.client + ) address_provider = implementations.address_provider instruction_builder = implementations.instruction_builder curve_manager = implementations.curve_manager @@ -60,10 +62,12 @@ class PlatformAwareBuyer(Trader): else: # Get pool address based on platform using platform-agnostic method pool_address = self._get_pool_address(token_info, address_provider) - + # Regular behavior with RPC call token_price_sol = await curve_manager.calculate_price(pool_address) - token_amount = self.amount / token_price_sol if token_price_sol > 0 else 0 + token_amount = ( + self.amount / token_price_sol if token_price_sol > 0 else 0 + ) # Calculate minimum token amount with slippage minimum_token_amount = token_amount * (1 - self.slippage) @@ -78,7 +82,7 @@ class PlatformAwareBuyer(Trader): self.wallet.pubkey, max_amount_lamports, # amount_in (SOL) minimum_token_amount_raw, # minimum_amount_out (tokens) - address_provider + address_provider, ) # Get accounts for priority fee calculation @@ -125,21 +129,21 @@ class PlatformAwareBuyer(Trader): except Exception as e: logger.exception("Buy operation failed") return TradeResult( - success=False, - platform=token_info.platform, - error_message=str(e) + success=False, platform=token_info.platform, error_message=str(e) ) - def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey: + def _get_pool_address( + self, token_info: TokenInfo, address_provider: AddressProvider + ) -> Pubkey: """Get the pool/curve address for price calculations using platform-agnostic method.""" # Try to get the address from token_info first, then derive if needed if token_info.platform == Platform.PUMP_FUN: - if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve: + if hasattr(token_info, "bonding_curve") and token_info.bonding_curve: return token_info.bonding_curve elif token_info.platform == Platform.LETS_BONK: - if hasattr(token_info, 'pool_state') and token_info.pool_state: + if hasattr(token_info, "pool_state") and token_info.pool_state: return token_info.pool_state - + # Fallback to deriving the address using platform provider return address_provider.derive_pool_address(token_info.mint) @@ -166,7 +170,9 @@ class PlatformAwareSeller(Trader): """Execute sell operation using platform-specific implementations.""" try: # Get platform-specific implementations - implementations = get_platform_implementations(token_info.platform, self.client) + implementations = get_platform_implementations( + token_info.platform, self.client + ) address_provider = implementations.address_provider instruction_builder = implementations.instruction_builder curve_manager = implementations.curve_manager @@ -175,8 +181,10 @@ class PlatformAwareSeller(Trader): user_token_account = address_provider.derive_user_token_account( self.wallet.pubkey, token_info.mint ) - - token_balance = await self.client.get_token_account_balance(user_token_account) + + token_balance = await self.client.get_token_account_balance( + user_token_account + ) token_balance_decimal = token_balance / 10**TOKEN_DECIMALS logger.info(f"Token balance: {token_balance_decimal}") @@ -184,9 +192,9 @@ class PlatformAwareSeller(Trader): if token_balance == 0: logger.info("No tokens to sell.") return TradeResult( - success=False, + success=False, platform=token_info.platform, - error_message="No tokens to sell" + error_message="No tokens to sell", ) # Get pool address and current price using platform-agnostic method @@ -197,9 +205,13 @@ class PlatformAwareSeller(Trader): # Calculate minimum SOL output with slippage expected_sol_output = float(token_balance_decimal) * float(token_price_sol) - min_sol_output = int((expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL) + min_sol_output = int( + (expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL + ) - logger.info(f"Selling {token_balance_decimal} tokens on {token_info.platform.value}") + logger.info( + f"Selling {token_balance_decimal} tokens on {token_info.platform.value}" + ) 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" @@ -211,7 +223,7 @@ class PlatformAwareSeller(Trader): self.wallet.pubkey, token_balance, # amount_in (tokens) min_sol_output, # minimum_amount_out (SOL) - address_provider + address_provider, ) # Get accounts for priority fee calculation @@ -251,20 +263,20 @@ class PlatformAwareSeller(Trader): except Exception as e: logger.exception("Sell operation failed") return TradeResult( - success=False, - platform=token_info.platform, - error_message=str(e) + success=False, platform=token_info.platform, error_message=str(e) ) - def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey: + def _get_pool_address( + self, token_info: TokenInfo, address_provider: AddressProvider + ) -> Pubkey: """Get the pool/curve address for price calculations using platform-agnostic method.""" # Try to get the address from token_info first, then derive if needed if token_info.platform == Platform.PUMP_FUN: - if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve: + if hasattr(token_info, "bonding_curve") and token_info.bonding_curve: return token_info.bonding_curve elif token_info.platform == Platform.LETS_BONK: - if hasattr(token_info, 'pool_state') and token_info.pool_state: + if hasattr(token_info, "pool_state") and token_info.pool_state: return token_info.pool_state - + # Fallback to deriving the address using platform provider - return address_provider.derive_pool_address(token_info.mint) \ No newline at end of file + return address_provider.derive_pool_address(token_info.mint) diff --git a/src/trading/universal_trader.py b/src/trading/universal_trader.py index 3b50da6..1b9a1f9 100644 --- a/src/trading/universal_trader.py +++ b/src/trading/universal_trader.py @@ -108,6 +108,7 @@ class UniversalTrader: # Validate platform support try: from platforms import platform_factory + if not platform_factory.registry.is_platform_supported(self.platform): raise ValueError(f"Platform {self.platform.value} is not supported") except Exception: @@ -130,7 +131,7 @@ class UniversalTrader: extreme_fast_token_amount, extreme_fast_mode, ) - + self.seller = PlatformAwareSeller( self.solana_client, self.wallet, @@ -193,17 +194,27 @@ class UniversalTrader: async def start(self) -> None: """Start the trading bot and listen for new tokens.""" logger.info(f"Starting Universal Trader for {self.platform.value}") - 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"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"Exit strategy: {self.exit_strategy}") - + if self.exit_strategy == "tp_sl": - logger.info(f"Take profit: {self.take_profit_percentage * 100 if self.take_profit_percentage else 'None'}%") - logger.info(f"Stop loss: {self.stop_loss_percentage * 100 if self.stop_loss_percentage else 'None'}%") - logger.info(f"Max hold time: {self.max_hold_time if self.max_hold_time else 'None'} seconds") - + logger.info( + f"Take profit: {self.take_profit_percentage * 100 if self.take_profit_percentage else 'None'}%" + ) + logger.info( + f"Stop loss: {self.stop_loss_percentage * 100 if self.stop_loss_percentage else 'None'}%" + ) + logger.info( + f"Max hold time: {self.max_hold_time if self.max_hold_time else 'None'} seconds" + ) + logger.info(f"Max token age: {self.max_token_age} seconds") try: @@ -216,16 +227,22 @@ class UniversalTrader: # 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") + 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...") + 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") + logger.info( + "Running in continuous mode - will process tokens until interrupted" + ) processor_task = asyncio.create_task(self._process_token_queue()) try: @@ -278,12 +295,16 @@ class UniversalTrader: # Wait for a token with a timeout try: - logger.info(f"Waiting for a suitable token (timeout: {self.token_wait_timeout}s)...") + 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") + logger.info( + f"Timed out after waiting {self.token_wait_timeout}s for a token" + ) return None finally: listener_task.cancel() @@ -327,7 +348,9 @@ class UniversalTrader: 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}) on {token_info.platform.value}") + logger.info( + f"Queued new token: {token_info.symbol} ({token_info.mint}) on {token_info.platform.value}" + ) async def _process_token_queue(self) -> None: """Continuously process tokens from the queue, only if they're fresh.""" @@ -338,15 +361,21 @@ class UniversalTrader: # Check if token is still "fresh" current_time = monotonic() - token_age = current_time - self.token_timestamps.get(token_key, current_time) + token_age = current_time - self.token_timestamps.get( + token_key, current_time + ) if token_age > self.max_token_age: - logger.info(f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)") + 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)") + logger.info( + f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)" + ) await self._handle_token(token_info) except asyncio.CancelledError: @@ -362,17 +391,23 @@ class UniversalTrader: try: # Validate that token is for our platform if token_info.platform != self.platform: - logger.warning(f"Token platform mismatch: expected {self.platform.value}, got {token_info.platform.value}") + logger.warning( + f"Token platform mismatch: expected {self.platform.value}, got {token_info.platform.value}" + ) return # Wait for pool/curve to stabilize (unless in extreme fast mode) if not self.extreme_fast_mode: await self._save_token_info(token_info) - logger.info(f"Waiting for {self.wait_time_after_creation} seconds for the pool/curve to stabilize...") + logger.info( + f"Waiting for {self.wait_time_after_creation} seconds for the pool/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} on {token_info.platform.value}...") + logger.info( + f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol} on {token_info.platform.value}..." + ) buy_result: TradeResult = await self.buyer.execute(token_info) if buy_result.success: @@ -382,16 +417,28 @@ class UniversalTrader: # 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...") + 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: logger.exception(f"Error handling token {token_info.symbol}") - async def _handle_successful_buy(self, token_info: TokenInfo, buy_result: TradeResult) -> None: + async def _handle_successful_buy( + self, token_info: TokenInfo, buy_result: TradeResult + ) -> None: """Handle successful token purchase.""" - logger.info(f"Successfully bought {token_info.symbol} on {token_info.platform.value}") - self._log_trade("buy", token_info, buy_result.price, buy_result.amount, buy_result.tx_signature) + logger.info( + f"Successfully bought {token_info.symbol} on {token_info.platform.value}" + ) + self._log_trade( + "buy", + token_info, + buy_result.price, + buy_result.amount, + buy_result.tx_signature, + ) self.traded_mints.add(token_info.mint) # Choose exit strategy @@ -405,7 +452,9 @@ class UniversalTrader: else: logger.info("Marry mode enabled. Skipping sell operation.") - async def _handle_failed_buy(self, token_info: TokenInfo, buy_result: TradeResult) -> None: + async def _handle_failed_buy( + self, token_info: TokenInfo, buy_result: TradeResult + ) -> None: """Handle failed token purchase.""" logger.error(f"Failed to buy {token_info.symbol}: {buy_result.error_message}") # Close ATA if enabled @@ -419,7 +468,9 @@ class UniversalTrader: self.cleanup_force_close_with_burn, ) - async def _handle_tp_sl_exit(self, token_info: TokenInfo, buy_result: TradeResult) -> None: + async def _handle_tp_sl_exit( + self, token_info: TokenInfo, buy_result: TradeResult + ) -> None: """Handle take profit/stop loss exit strategy.""" # Create position position = Position.create_from_buy_result( @@ -451,7 +502,13 @@ class UniversalTrader: if sell_result.success: logger.info(f"Successfully sold {token_info.symbol}") - self._log_trade("sell", token_info, sell_result.price, sell_result.amount, sell_result.tx_signature) + self._log_trade( + "sell", + token_info, + sell_result.price, + sell_result.amount, + sell_result.tx_signature, + ) # Close ATA if enabled await handle_cleanup_after_sell( self.solana_client, @@ -463,11 +520,17 @@ class UniversalTrader: self.cleanup_force_close_with_burn, ) else: - logger.error(f"Failed to sell {token_info.symbol}: {sell_result.error_message}") + logger.error( + f"Failed to sell {token_info.symbol}: {sell_result.error_message}" + ) - async def _monitor_position_until_exit(self, token_info: TokenInfo, position: Position) -> None: + async def _monitor_position_until_exit( + self, token_info: TokenInfo, position: Position + ) -> None: """Monitor a position until exit conditions are met.""" - logger.info(f"Starting position monitoring (check interval: {self.price_check_interval}s)") + logger.info( + f"Starting position monitoring (check interval: {self.price_check_interval}s)" + ) # Get pool address for price monitoring using platform-agnostic method pool_address = self._get_pool_address(token_info) @@ -487,7 +550,9 @@ class UniversalTrader: # Log PnL before exit pnl = position.get_pnl(current_price) - logger.info(f"Position PnL: {pnl['price_change_pct']:.2f}% ({pnl['unrealized_pnl_sol']:.6f} SOL)") + logger.info( + f"Position PnL: {pnl['price_change_pct']:.2f}% ({pnl['unrealized_pnl_sol']:.6f} SOL)" + ) # Execute sell sell_result = await self.seller.execute(token_info) @@ -496,12 +561,22 @@ class UniversalTrader: # Close position with actual exit price position.close_position(sell_result.price, exit_reason) - logger.info(f"Successfully exited position: {exit_reason.value}") - self._log_trade("sell", token_info, sell_result.price, sell_result.amount, sell_result.tx_signature) + logger.info( + f"Successfully exited position: {exit_reason.value}" + ) + self._log_trade( + "sell", + token_info, + sell_result.price, + sell_result.amount, + sell_result.tx_signature, + ) # Log final PnL final_pnl = position.get_pnl() - logger.info(f"Final PnL: {final_pnl['price_change_pct']:.2f}% ({final_pnl['unrealized_pnl_sol']:.6f} SOL)") + logger.info( + f"Final PnL: {final_pnl['price_change_pct']:.2f}% ({final_pnl['unrealized_pnl_sol']:.6f} SOL)" + ) # Close ATA if enabled await handle_cleanup_after_sell( @@ -514,30 +589,36 @@ class UniversalTrader: self.cleanup_force_close_with_burn, ) else: - logger.error(f"Failed to exit position: {sell_result.error_message}") + logger.error( + f"Failed to exit position: {sell_result.error_message}" + ) # Keep monitoring in case sell can be retried break else: # Log current status pnl = position.get_pnl(current_price) - logger.debug(f"Position status: {current_price:.8f} SOL ({pnl['price_change_pct']:+.2f}%)") + logger.debug( + f"Position status: {current_price:.8f} SOL ({pnl['price_change_pct']:+.2f}%)" + ) # Wait before next price check await asyncio.sleep(self.price_check_interval) except Exception: logger.exception("Error monitoring position") - await asyncio.sleep(self.price_check_interval) # Continue monitoring despite errors + await asyncio.sleep( + self.price_check_interval + ) # Continue monitoring despite errors def _get_pool_address(self, token_info: TokenInfo) -> Pubkey: """Get the pool/curve address for price monitoring using platform-agnostic method.""" address_provider = self.platform_implementations.address_provider - + # Use platform-specific logic to get the appropriate address - if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve: + if hasattr(token_info, "bonding_curve") and token_info.bonding_curve: return token_info.bonding_curve - elif hasattr(token_info, 'pool_state') and token_info.pool_state: + elif hasattr(token_info, "pool_state") and token_info.pool_state: return token_info.pool_state else: # Fallback to deriving the address using platform provider @@ -561,7 +642,7 @@ class UniversalTrader: "creator": str(token_info.creator) if token_info.creator else None, "creation_timestamp": token_info.creation_timestamp, } - + # Add platform-specific fields only if they exist platform_fields = { "bonding_curve": token_info.bonding_curve, @@ -571,7 +652,7 @@ class UniversalTrader: "base_vault": token_info.base_vault, "quote_vault": token_info.quote_vault, } - + for field_name, field_value in platform_fields.items(): if field_value is not None: token_dict[field_name] = str(field_value) @@ -582,7 +663,14 @@ class UniversalTrader: except OSError: logger.exception("Failed to save token information") - def _log_trade(self, action: str, token_info: TokenInfo, price: float, amount: float, tx_hash: str | None) -> None: + def _log_trade( + self, + action: str, + token_info: TokenInfo, + price: float, + amount: float, + tx_hash: str | None, + ) -> None: """Log trade information.""" try: trades_dir = Path("trades") @@ -607,4 +695,4 @@ class UniversalTrader: # Backward compatibility alias -PumpTrader = UniversalTrader # Legacy name for backward compatibility \ No newline at end of file +PumpTrader = UniversalTrader # Legacy name for backward compatibility diff --git a/src/utils/idl_manager.py b/src/utils/idl_manager.py index e8fb69a..977e79a 100644 --- a/src/utils/idl_manager.py +++ b/src/utils/idl_manager.py @@ -17,85 +17,91 @@ logger = get_logger(__name__) class IDLManager: """Centralized manager for IDL parsers across all platforms.""" - + def __init__(self): """Initialize the IDL manager.""" self._parsers: dict[Platform, IDLParser] = {} self._idl_paths: dict[Platform, str] = {} self._setup_platform_idl_paths() - + def _setup_platform_idl_paths(self) -> None: """Setup IDL file paths for each platform.""" # Get the project root directory (3 levels up from this file) current_file = Path(__file__) project_root = current_file.parent.parent.parent - + # Define IDL paths for each platform self._idl_paths = { Platform.LETS_BONK: project_root / "idl" / "raydium_launchlab_idl.json", Platform.PUMP_FUN: project_root / "idl" / "pump_fun_idl.json", } - + def get_parser(self, platform: Platform, verbose: bool = False) -> IDLParser: """Get or create an IDL parser for the specified platform. - + Args: platform: Platform to get parser for verbose: Whether to enable verbose logging in the parser - + Returns: IDLParser instance for the platform - + Raises: ValueError: If platform is not supported or IDL file not found """ # Return cached parser if available if platform in self._parsers: return self._parsers[platform] - + # Check if platform has IDL support if platform not in self._idl_paths: - raise ValueError(f"Platform {platform.value} does not have IDL support configured") - + raise ValueError( + f"Platform {platform.value} does not have IDL support configured" + ) + idl_path = self._idl_paths[platform] - + # Verify IDL file exists if not idl_path.exists(): - raise FileNotFoundError(f"IDL file not found for {platform.value} at {idl_path}") - + raise FileNotFoundError( + f"IDL file not found for {platform.value} at {idl_path}" + ) + # Load and cache the parser logger.info(f"Loading IDL parser for {platform.value} from {idl_path}") parser = IDLParser(idl_path, verbose=verbose) self._parsers[platform] = parser - + instruction_count = len(parser.get_instruction_names()) event_count = len(parser.get_event_names()) - logger.info(f"IDL parser loaded for {platform.value} with {instruction_count} instructions and {event_count} events") - + logger.info( + f"IDL parser loaded for {platform.value} with {instruction_count} instructions and {event_count} events" + ) + return parser - + def has_idl_support(self, platform: Platform) -> bool: """Check if a platform has IDL support configured. - + Args: platform: Platform to check - + Returns: True if platform has IDL support """ return platform in self._idl_paths - + def get_supported_platforms(self) -> list[Platform]: """Get list of platforms with IDL support. - + Returns: List of platforms that have IDL files configured """ return list(self._idl_paths.keys()) - + def clear_cache(self, platform: Platform | None = None) -> None: """Clear cached parsers. - + Args: platform: Specific platform to clear, or None to clear all """ @@ -105,12 +111,12 @@ class IDLManager: elif platform in self._parsers: logger.info(f"Clearing cached IDL parser for {platform.value}") del self._parsers[platform] - + def preload_parser(self, platform: Platform, verbose: bool = False) -> None: """Preload IDL parser for a platform. - + This can be useful for warming up the parser during initialization. - + Args: platform: Platform to preload parser for verbose: Whether to enable verbose logging in the parser @@ -120,101 +126,105 @@ class IDLManager: self.get_parser(platform, verbose) else: logger.debug(f"IDL parser for {platform.value} already loaded") - + # -------------------------------------------------------------------------- # Instruction-related convenience methods # -------------------------------------------------------------------------- - + def get_instruction_discriminators(self, platform: Platform) -> dict[str, bytes]: """Get instruction discriminators for a platform. - + Args: platform: Platform to get discriminators for - + Returns: Dictionary mapping instruction names to discriminator bytes """ parser = self.get_parser(platform) return parser.get_instruction_discriminators() - + def get_instruction_names(self, platform: Platform) -> list[str]: """Get available instruction names for a platform. - + Args: platform: Platform to get instruction names for - + Returns: List of instruction names """ parser = self.get_parser(platform) return parser.get_instruction_names() - + # -------------------------------------------------------------------------- # Event-related convenience methods # -------------------------------------------------------------------------- - + def get_event_discriminators(self, platform: Platform) -> dict[str, bytes]: """Get event discriminators for a platform. - + Args: platform: Platform to get event discriminators for - + Returns: Dictionary mapping event names to discriminator bytes """ parser = self.get_parser(platform) return parser.get_event_discriminators() - + def get_event_names(self, platform: Platform) -> list[str]: """Get available event names for a platform. - + Args: platform: Platform to get event names for - + Returns: List of event names """ parser = self.get_parser(platform) return parser.get_event_names() - - def decode_event_from_logs(self, platform: Platform, logs: list[str], event_name: str | None = None) -> dict | None: + + def decode_event_from_logs( + self, platform: Platform, logs: list[str], event_name: str | None = None + ) -> dict | None: """Decode event data from transaction logs for a platform. - + Args: platform: Platform to use for decoding logs: List of log strings from transaction event_name: Optional specific event name to look for - + Returns: Decoded event data if found, None otherwise """ parser = self.get_parser(platform) return parser.find_event_in_logs(logs, event_name) - - def decode_event_data(self, platform: Platform, event_data: bytes, event_name: str | None = None) -> dict | None: + + def decode_event_data( + self, platform: Platform, event_data: bytes, event_name: str | None = None + ) -> dict | None: """Decode raw event data for a platform. - + Args: platform: Platform to use for decoding event_data: Raw event data bytes event_name: Optional event name to decode as - + Returns: Decoded event data if successful, None otherwise """ parser = self.get_parser(platform) return parser.decode_event_data(event_data, event_name) - + # -------------------------------------------------------------------------- # Platform information methods # -------------------------------------------------------------------------- - + def get_platform_capabilities(self, platform: Platform) -> dict[str, Any]: """Get comprehensive capability information for a platform. - + Args: platform: Platform to get capabilities for - + Returns: Dictionary with platform capabilities """ @@ -227,12 +237,12 @@ class IDLManager: "instruction_count": 0, "event_count": 0, } - + try: parser = self.get_parser(platform) instruction_names = parser.get_instruction_names() event_names = parser.get_event_names() - + return { "platform": platform.value, "has_idl_support": True, @@ -260,7 +270,7 @@ _idl_manager: IDLManager | None = None def get_idl_manager() -> IDLManager: """Get the global IDL manager instance. - + Returns: Global IDLManager instance """ @@ -272,11 +282,11 @@ def get_idl_manager() -> IDLManager: def get_idl_parser(platform: Platform, verbose: bool = False) -> IDLParser: """Convenience function to get an IDL parser for a platform. - + Args: platform: Platform to get parser for verbose: Whether to enable verbose logging in the parser - + Returns: IDLParser instance for the platform """ @@ -285,10 +295,10 @@ def get_idl_parser(platform: Platform, verbose: bool = False) -> IDLParser: def has_idl_support(platform: Platform) -> bool: """Check if a platform has IDL support. - + Args: platform: Platform to check - + Returns: True if platform has IDL support """ @@ -297,7 +307,7 @@ def has_idl_support(platform: Platform) -> bool: def preload_platform_idl(platform: Platform, verbose: bool = False) -> None: """Preload IDL parser for a platform. - + Args: platform: Platform to preload parser for verbose: Whether to enable verbose logging @@ -309,12 +319,13 @@ def preload_platform_idl(platform: Platform, verbose: bool = False) -> None: # Convenience functions for event handling # -------------------------------------------------------------------------- + def get_event_discriminators(platform: Platform) -> dict[str, bytes]: """Convenience function to get event discriminators for a platform. - + Args: platform: Platform to get event discriminators for - + Returns: Dictionary mapping event names to discriminator bytes """ @@ -323,25 +334,27 @@ def get_event_discriminators(platform: Platform) -> dict[str, bytes]: def get_event_names(platform: Platform) -> list[str]: """Convenience function to get event names for a platform. - + Args: platform: Platform to get event names for - + Returns: List of event names """ return get_idl_manager().get_event_names(platform) -def decode_event_from_logs(platform: Platform, logs: list[str], event_name: str | None = None) -> dict | None: +def decode_event_from_logs( + platform: Platform, logs: list[str], event_name: str | None = None +) -> dict | None: """Convenience function to decode events from logs. - + Args: platform: Platform to use for decoding logs: List of log strings from transaction event_name: Optional specific event name to look for - + Returns: Decoded event data if found, None otherwise """ - return get_idl_manager().decode_event_from_logs(platform, logs, event_name) \ No newline at end of file + return get_idl_manager().decode_event_from_logs(platform, logs, event_name) diff --git a/src/utils/idl_parser.py b/src/utils/idl_parser.py index 2154b9d..5961986 100644 --- a/src/utils/idl_parser.py +++ b/src/utils/idl_parser.py @@ -24,17 +24,20 @@ class IDLParser: # to its struct format character and size in bytes. _PRIMITIVE_TYPE_INFO = { # type_name: (format_char, size_in_bytes) - 'u8': (' dict[str, bytes]: """Get a mapping of instruction names to their discriminators.""" - return {instr['name']: disc for disc, instr in self.instructions.items()} + return {instr["name"]: disc for disc, instr in self.instructions.items()} def get_instruction_names(self) -> list[str]: """Get a list of all available instruction names.""" - return [instr['name'] for instr in self.instructions.values()] + return [instr["name"] for instr in self.instructions.values()] - def validate_instruction_data_length(self, ix_data: bytes, discriminator: bytes) -> bool: + def validate_instruction_data_length( + self, ix_data: bytes, discriminator: bytes + ) -> bool: """Validate that instruction data meets minimum length requirements.""" if discriminator not in self.instruction_min_sizes: return True # Allow if we don't know the expected size @@ -78,7 +83,7 @@ class IDLParser: actual_size = len(ix_data) if actual_size < expected_min_size: - instruction_name = self.instructions[discriminator]['name'] + instruction_name = self.instructions[discriminator]["name"] if self.verbose: print( f"⚠️ Instruction data for '{instruction_name}' is shorter than the expected minimum " @@ -88,7 +93,9 @@ class IDLParser: return True - def decode_instruction(self, ix_data: bytes, keys: list[bytes], accounts: list[int]) -> dict[str, Any] | None: + def decode_instruction( + self, ix_data: bytes, keys: list[bytes], accounts: list[int] + ) -> dict[str, Any] | None: """Decode instruction data using IDL definitions.""" if len(ix_data) < DISCRIMINATOR_SIZE: return None @@ -102,14 +109,16 @@ class IDLParser: instruction = self.instructions[discriminator] data_args = ix_data[DISCRIMINATOR_SIZE:] - + # Decode instruction arguments args = {} decode_offset = 0 - for arg in instruction.get('args', []): + for arg in instruction.get("args", []): try: - value, decode_offset = self._decode_type(data_args, decode_offset, arg['type']) - args[arg['name']] = value + value, decode_offset = self._decode_type( + data_args, decode_offset, arg["type"] + ) + args[arg["name"]] = value except Exception as e: if self.verbose: print(f"❌ Decode error in argument '{arg['name']}': {e}") @@ -120,19 +129,19 @@ class IDLParser: if index < len(accounts): account_index = accounts[index] if account_index < len(keys): - return base58.b58encode(keys[account_index]).decode('utf-8') - return None # Return None for invalid indices + return base58.b58encode(keys[account_index]).decode("utf-8") + return None # Return None for invalid indices # Build account info based on instruction definition account_info = {} - instruction_accounts = instruction.get('accounts', []) + instruction_accounts = instruction.get("accounts", []) for i, account_def in enumerate(instruction_accounts): - account_info[account_def['name']] = get_account_key(i) + account_info[account_def["name"]] = get_account_key(i) return { - 'instruction_name': instruction['name'], - 'args': args, - 'accounts': account_info + "instruction_name": instruction["name"], + "args": args, + "accounts": account_info, } # -------------------------------------------------------------------------- @@ -141,20 +150,22 @@ class IDLParser: def get_event_discriminators(self) -> dict[str, bytes]: """Get a mapping of event names to their discriminators.""" - return {event['name']: disc for disc, event in self.events.items()} + return {event["name"]: disc for disc, event in self.events.items()} def get_event_names(self) -> list[str]: """Get a list of all available event names.""" - return [event['name'] for event in self.events.values()] + return [event["name"] for event in self.events.values()] - def decode_event_data(self, event_data: bytes, event_name: str | None = None) -> dict[str, Any] | None: + def decode_event_data( + self, event_data: bytes, event_name: str | None = None + ) -> dict[str, Any] | None: """ Decode event data using IDL event definitions. - + Args: event_data: Raw event data bytes (typically from base64 decoded log data) event_name: Optional event name to decode as. If None, will try to match discriminator. - + Returns: Decoded event data as a dictionary, or None if decoding fails. """ @@ -162,89 +173,94 @@ class IDLParser: return None discriminator = event_data[:DISCRIMINATOR_SIZE] - + # Find event definition by discriminator in events section if discriminator not in self.events: if self.verbose: print(f"Unknown event discriminator: {discriminator.hex()}") return None - + event_def = self.events[discriminator] - + # If event_name provided, validate it matches - if event_name and event_def['name'] != event_name: + if event_name and event_def["name"] != event_name: if self.verbose: - print(f"Event name mismatch: expected {event_name}, got {event_def['name']}") + print( + f"Event name mismatch: expected {event_name}, got {event_def['name']}" + ) return None # Get the actual structure definition from types section - event_name_actual = event_def['name'] + event_name_actual = event_def["name"] if event_name_actual not in self.types: if self.verbose: print(f"Event type {event_name_actual} not found in types section") return None - + type_def = self.types[event_name_actual] - event_type = type_def.get('type', {}) - + event_type = type_def.get("type", {}) + # Decode event fields try: event_fields = {} data_part = event_data[DISCRIMINATOR_SIZE:] decode_offset = 0 - - if event_type.get('kind') != 'struct': + + if event_type.get("kind") != "struct": if self.verbose: - print(f"Event {event_name_actual} is not a struct type: {event_type.get('kind', 'NO KIND')}") + print( + f"Event {event_name_actual} is not a struct type: {event_type.get('kind', 'NO KIND')}" + ) print(f"Available keys in type_def: {list(type_def.keys())}") print(f"Event type structure: {event_type}") return None - + # Decode each field in the struct - fields = event_type.get('fields', []) + fields = event_type.get("fields", []) if self.verbose: print(f"Decoding {len(fields)} fields for event {event_name_actual}") - + for field in fields: if self.verbose: print(f"Decoding field: {field['name']} ({field['type']})") - + try: - value, decode_offset = self._decode_type(data_part, decode_offset, field['type']) - event_fields[field['name']] = value - + value, decode_offset = self._decode_type( + data_part, decode_offset, field["type"] + ) + event_fields[field["name"]] = value + if self.verbose: - if field['type'] == 'string': + if field["type"] == "string": print(f" -> '{value}'") - elif field['type'] == 'pubkey': + elif field["type"] == "pubkey": print(f" -> {value}") else: print(f" -> {value}") - + except Exception as e: if self.verbose: print(f"Error decoding field {field['name']}: {e}") # Don't return None here, continue with other fields continue - return { - 'event_name': event_name_actual, - 'fields': event_fields - } + return {"event_name": event_name_actual, "fields": event_fields} except Exception as e: if self.verbose: print(f"❌ Error decoding event {event_name_actual}: {e}") return None - def find_event_in_logs(self, logs: list[str], target_event_name: str | None = None) -> dict[str, Any] | None: + def find_event_in_logs( + self, logs: list[str], target_event_name: str | None = None + ) -> dict[str, Any] | None: """ Find and decode event data from transaction logs. - + Args: logs: List of log strings from a transaction target_event_name: Optional specific event name to look for - + Returns: Decoded event data if found, None otherwise """ @@ -254,34 +270,39 @@ class IDLParser: # Extract base64 encoded data encoded_data = log.split("Program data: ")[1].strip() decoded_data = base64.b64decode(encoded_data) - + # Try to decode as event event_data = self.decode_event_data(decoded_data, target_event_name) if event_data: return event_data - + except Exception as e: if self.verbose: print(f"Failed to decode log data: {e}") continue - + return None # -------------------------------------------------------------------------- # Public Methods (External API) - Account Data # -------------------------------------------------------------------------- - def decode_account_data(self, account_data: bytes, account_type_name: str, skip_discriminator: bool = True) -> dict[str, Any] | None: + def decode_account_data( + self, + account_data: bytes, + account_type_name: str, + skip_discriminator: bool = True, + ) -> dict[str, Any] | None: """ Decode account data using a specific account type from the IDL. - + Args: account_data: Raw account data bytes. account_type_name: Name of the account type in the IDL (e.g., "MyAccount"). skip_discriminator: Whether to skip the first 8 bytes, which Anchor uses as a type discriminator for account data. Set to False if your data does not have this prefix. - + Returns: Decoded account data as a dictionary, or None if decoding fails. """ @@ -295,7 +316,9 @@ class IDLParser: if skip_discriminator: if len(account_data) < DISCRIMINATOR_SIZE: if self.verbose: - print(f"Account data too short to contain a discriminator: {len(account_data)} bytes") + print( + f"Account data too short to contain a discriminator: {len(account_data)} bytes" + ) return None data = account_data[DISCRIMINATOR_SIZE:] @@ -313,34 +336,36 @@ class IDLParser: def _build_instruction_map(self): """Build a map of discriminators to instruction definitions.""" - for instruction in self.idl.get('instructions', []): + for instruction in self.idl.get("instructions", []): # The discriminator from the JSON IDL is a list of u8 integers. - discriminator = bytes(instruction['discriminator']) + discriminator = bytes(instruction["discriminator"]) self.instructions[discriminator] = instruction def _build_event_map(self): """Build a map of discriminators to event definitions.""" - for event in self.idl.get('events', []): + for event in self.idl.get("events", []): # The discriminator from the JSON IDL is a list of u8 integers. - discriminator = bytes(event['discriminator']) + discriminator = bytes(event["discriminator"]) self.events[discriminator] = event if self.verbose: - print(f"📅 Loaded event: {event['name']} with discriminator {discriminator.hex()}") + print( + f"📅 Loaded event: {event['name']} with discriminator {discriminator.hex()}" + ) def _build_type_map(self): """Build a map of type names to their definitions.""" - for type_def in self.idl.get('types', []): - self.types[type_def['name']] = type_def + for type_def in self.idl.get("types", []): + self.types[type_def["name"]] = type_def def _calculate_instruction_sizes(self): """Calculate minimum data sizes for each instruction.""" for discriminator, instruction in self.instructions.items(): try: min_size = DISCRIMINATOR_SIZE - for arg in instruction.get('args', []): - min_size += self._calculate_type_min_size(arg['type']) + for arg in instruction.get("args", []): + min_size += self._calculate_type_min_size(arg["type"]) self.instruction_min_sizes[discriminator] = min_size - if self.verbose and instruction['name'] == 'initialize': + if self.verbose and instruction["name"] == "initialize": print(f"📏 Initialize instruction min size: {min_size} bytes") except Exception as e: if self.verbose: @@ -351,17 +376,19 @@ class IDLParser: """Calculate minimum size in bytes for a type definition.""" if isinstance(type_def, str): return self._get_primitive_size(type_def) - + if isinstance(type_def, dict): - if 'defined' in type_def: + if "defined" in type_def: type_name = self._get_defined_type_name(type_def) return self._calculate_defined_type_min_size(type_name) - if 'array' in type_def: - element_type, array_length = type_def['array'] + if "array" in type_def: + element_type, array_length = type_def["array"] element_size = self._calculate_type_min_size(element_type) return element_size * array_length - - raise ValueError(f"Invalid or unknown type definition for size calculation: {type_def}") + + raise ValueError( + f"Invalid or unknown type definition for size calculation: {type_def}" + ) def _get_primitive_size(self, type_name: str) -> int: """Get size in bytes for primitive types from the central map.""" @@ -370,51 +397,62 @@ class IDLParser: def _get_defined_type_name(self, type_def: dict[str, Any]) -> str: """Extracts the type name from a 'defined' type, handling old and new IDL formats.""" - defined_value = type_def['defined'] + defined_value = type_def["defined"] # New format: {'defined': {'name': 'MyType'}} # Old format: {'defined': 'MyType'} - return defined_value['name'] if isinstance(defined_value, dict) else defined_value + return ( + defined_value["name"] if isinstance(defined_value, dict) else defined_value + ) def _calculate_defined_type_min_size(self, type_name: str) -> int: """Calculate minimum size for user-defined types (structs and enums).""" if type_name not in self.types: raise ValueError(f"Unknown defined type: {type_name}") - - type_def = self.types[type_name]['type'] - - if type_def['kind'] == 'struct': - return sum(self._calculate_type_min_size(field['type']) for field in type_def['fields']) - - if type_def['kind'] == 'enum': + + type_def = self.types[type_name]["type"] + + if type_def["kind"] == "struct": + return sum( + self._calculate_type_min_size(field["type"]) + for field in type_def["fields"] + ) + + if type_def["kind"] == "enum": # The size of an enum is its discriminator plus the size of its LARGEST variant, # as the data layout must accommodate any possible variant. max_variant_size = 0 - for variant in type_def['variants']: + for variant in type_def["variants"]: variant_size = 0 - for field in variant.get('fields', []): + for field in variant.get("fields", []): # A field can be a type string/dict (tuple variant) or a dict with a 'type' key (struct variant) - field_type = field['type'] if isinstance(field, dict) else field + field_type = field["type"] if isinstance(field, dict) else field variant_size += self._calculate_type_min_size(field_type) max_variant_size = max(max_variant_size, variant_size) return ENUM_DISCRIMINATOR_SIZE + max_variant_size - raise ValueError(f"Unsupported type kind for size calculation: {type_def['kind']}") + raise ValueError( + f"Unsupported type kind for size calculation: {type_def['kind']}" + ) - def _decode_type(self, data: bytes, offset: int, type_def: str | dict) -> tuple[Any, int]: + def _decode_type( + self, data: bytes, offset: int, type_def: str | dict + ) -> tuple[Any, int]: """Decode a value based on its type definition.""" if isinstance(type_def, str): return self._decode_primitive(data, offset, type_def) - + if isinstance(type_def, dict): - if 'defined' in type_def: + if "defined" in type_def: type_name = self._get_defined_type_name(type_def) return self._decode_defined_type(data, offset, type_name) - if 'array' in type_def: - return self._decode_array(data, offset, type_def['array']) - + if "array" in type_def: + return self._decode_array(data, offset, type_def["array"]) + raise ValueError(f"Invalid or unknown type definition for decoding: {type_def}") - def _decode_array(self, data: bytes, offset: int, array_def: list) -> tuple[list[Any], int]: + def _decode_array( + self, data: bytes, offset: int, array_def: list + ) -> tuple[list[Any], int]: """Decode fixed-size array types.""" element_type, array_length = array_def array_data = [] @@ -423,20 +461,22 @@ class IDLParser: array_data.append(value) return array_data, offset - def _decode_primitive(self, data: bytes, offset: int, type_name: str) -> tuple[Any, int]: + def _decode_primitive( + self, data: bytes, offset: int, type_name: str + ) -> tuple[Any, int]: """Decode primitive types.""" if type_name not in self._PRIMITIVE_TYPE_INFO: raise ValueError(f"Unknown primitive type: {type_name}") - if type_name == 'string': - length = struct.unpack_from(' tuple[dict[str, Any], int]: + def _decode_defined_type( + self, data: bytes, offset: int, type_name: str + ) -> tuple[dict[str, Any], int]: """Decode user-defined types (structs and enums).""" if type_name not in self.types: raise ValueError(f"Unknown defined type: {type_name}") - - type_def = self.types[type_name]['type'] - if type_def['kind'] == 'struct': + type_def = self.types[type_name]["type"] + + if type_def["kind"] == "struct": struct_data = {} - for field in type_def['fields']: - value, offset = self._decode_type(data, offset, field['type']) - struct_data[field['name']] = value + for field in type_def["fields"]: + value, offset = self._decode_type(data, offset, field["type"]) + struct_data[field["name"]] = value return struct_data, offset - - if type_def['kind'] == 'enum': - variant_index = struct.unpack_from('= len(variants): - raise ValueError(f"Invalid enum variant index {variant_index} for type {type_name}") - + raise ValueError( + f"Invalid enum variant index {variant_index} for type {type_name}" + ) + variant = variants[variant_index] - result = {"variant": variant['name']} - variant_fields = variant.get('fields', []) + result = {"variant": variant["name"]} + variant_fields = variant.get("fields", []) if variant_fields: # Check if it's a struct variant (fields are dicts) or tuple variant (fields are strings/dicts) if isinstance(variant_fields[0], dict): struct_data = {} for field in variant_fields: - value, offset = self._decode_type(data, offset, field['type']) - struct_data[field['name']] = value - result['data'] = struct_data - else: # Tuple variant + value, offset = self._decode_type(data, offset, field["type"]) + struct_data[field["name"]] = value + result["data"] = struct_data + else: # Tuple variant tuple_data = [] for field_type in variant_fields: value, offset = self._decode_type(data, offset, field_type) tuple_data.append(value) - result['data'] = tuple_data - + result["data"] = tuple_data + return result, offset raise ValueError(f"Unsupported type kind for decoding: {type_def['kind']}") @@ -501,4 +545,4 @@ def load_idl_parser(idl_path: str, verbose: bool = False) -> IDLParser: Returns: Initialized IDLParser instance """ - return IDLParser(idl_path, verbose) \ No newline at end of file + return IDLParser(idl_path, verbose)