From bb35101a8f6b34ce8cd70c0aa5a897d79499aab2 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 09:25:39 +0000 Subject: [PATCH 01/30] wip(core): platform aware trading --- .cursor/rules/bots.mdc | 130 ------ .cursor/rules/cleanup.mdc | 68 --- .cursor/rules/core.mdc | 76 ---- .cursor/rules/idl.mdc | 89 ---- .cursor/rules/monitoring.mdc | 96 ----- .cursor/rules/package-management.mdc | 89 ---- .cursor/rules/project-structure.mdc | 115 ----- .cursor/rules/tests.mdc | 153 ------- .cursor/rules/trading.mdc | 92 ---- .cursor/rules/utils.mdc | 44 -- CLAUDE.md | 120 ------ bots/bot-sniper-1-geyser.yaml | 3 + bots/bot-sniper-2-logs.yaml | 3 + bots/bot-sniper-3-blocks.yaml | 3 + bots/bot-sniper-4-pp.yaml | 3 + src/config_loader.py | 81 +++- src/interfaces/__init__.py | 280 ++++++++++++ src/interfaces/core.py | 363 ++++++++++++++++ src/platforms/__init__.py | 282 ++++++++++++ src/platforms/letsbonk/__init__.py | 31 ++ src/platforms/letsbonk/address_provider.py | 244 +++++++++++ src/platforms/letsbonk/curve_manager.py | 254 +++++++++++ src/platforms/letsbonk/event_parser.py | 272 ++++++++++++ src/platforms/letsbonk/instruction_builder.py | 402 ++++++++++++++++++ src/platforms/pumpfun/__init__.py | 31 ++ src/platforms/pumpfun/address_provider.py | 216 ++++++++++ src/platforms/pumpfun/curve_manager.py | 215 ++++++++++ src/platforms/pumpfun/event_parser.py | 335 +++++++++++++++ src/platforms/pumpfun/instruction_builder.py | 234 ++++++++++ src/trading/base.py | 362 +++++++++++++++- src/trading/platform_aware.py | 315 ++++++++++++++ 31 files changed, 3907 insertions(+), 1094 deletions(-) delete mode 100644 .cursor/rules/bots.mdc delete mode 100644 .cursor/rules/cleanup.mdc delete mode 100644 .cursor/rules/core.mdc delete mode 100644 .cursor/rules/idl.mdc delete mode 100644 .cursor/rules/monitoring.mdc delete mode 100644 .cursor/rules/package-management.mdc delete mode 100644 .cursor/rules/project-structure.mdc delete mode 100644 .cursor/rules/tests.mdc delete mode 100644 .cursor/rules/trading.mdc delete mode 100644 .cursor/rules/utils.mdc delete mode 100644 CLAUDE.md create mode 100644 src/interfaces/__init__.py create mode 100644 src/interfaces/core.py create mode 100644 src/platforms/__init__.py create mode 100644 src/platforms/letsbonk/__init__.py create mode 100644 src/platforms/letsbonk/address_provider.py create mode 100644 src/platforms/letsbonk/curve_manager.py create mode 100644 src/platforms/letsbonk/event_parser.py create mode 100644 src/platforms/letsbonk/instruction_builder.py create mode 100644 src/platforms/pumpfun/__init__.py create mode 100644 src/platforms/pumpfun/address_provider.py create mode 100644 src/platforms/pumpfun/curve_manager.py create mode 100644 src/platforms/pumpfun/event_parser.py create mode 100644 src/platforms/pumpfun/instruction_builder.py create mode 100644 src/trading/platform_aware.py diff --git a/.cursor/rules/bots.mdc b/.cursor/rules/bots.mdc deleted file mode 100644 index 4046f5c..0000000 --- a/.cursor/rules/bots.mdc +++ /dev/null @@ -1,130 +0,0 @@ ---- -description: Comprehensive configuration guide for trading bots in the pump-bot system. Apply when configuring bots. -globs: bots/*.yaml -alwaysApply: true ---- - -# Bot Configuration Guide - -## Overview - -- YAML files in the `bots/` directory define configurations for different bot strategies and behaviors. -- Each file specifies parameters for a specific bot instance or strategy. -- Configurations control execution parameters, trading strategies, risk management, and connection settings. - -## Required Configuration Sections - -### Bot Identification and Connection Settings - -```yaml -name: "bot-name" # Unique identifier for the bot -env_file: ".env" # Environment file for secrets -rpc_endpoint: "${ENV_VAR}" # Solana RPC endpoint -wss_endpoint: "${ENV_VAR}" # Solana WebSocket endpoint -private_key: "${ENV_VAR}" # Private key for transactions -enabled: true # Enable/disable bot without removing config -separate_process: true # Run in a separate process -``` - -### Trading Parameters - -```yaml -trade: - buy_amount: 0.0001 # Amount of SOL to spend (in SOL) - buy_slippage: 0.3 # Maximum price deviation for buys (0.3 = 30%) - sell_slippage: 0.3 # Maximum price deviation for sells (0.3 = 30%) - - # Exit strategy - exit_strategy: "time_based" # Options: "time_based", "tp_sl", "manual" - take_profit_percentage: 0.2 # Required for "tp_sl" strategy (0.2 = 20%) - stop_loss_percentage: 0.2 # Required for "tp_sl" strategy (0.2 = 20%) - max_hold_time: 60 # Maximum hold time in seconds -``` - -### Listener Configuration - -Specify one of the following listener types: - -#### Geyser (Fastest Updates) - -```yaml -geyser: - endpoint: "${ENV_VAR}" - api_token: "${ENV_VAR}" - auth_type: "x-token" # or "basic" -``` - -#### Logs Listener - -```yaml -logs: - program_id: "${PROGRAM_ID}" # Program to monitor - refresh_interval: 1 # Polling interval in seconds -``` - -#### Blocks Listener - -```yaml -blocks: - refresh_interval: 1 # Block polling interval in seconds -``` - -#### PumpPortal Listener - -```yaml -pumpportal: - endpoint: "${PP_ENDPOINT}" # PumpPortal API endpoint - api_key: "${PP_API_KEY}" # API key for authentication -``` - -### Filters - -```yaml -filters: - listener_type: "geyser" # One of: "logs", "blocks", "geyser", "pumpportal" - max_token_age: 10 # Maximum age of tokens to consider (in seconds) - min_liquidity_sol: 0.1 # Minimum liquidity in SOL - exclude_tokens: ["TOKEN1", "TOKEN2"] # Tokens to exclude -``` - -### Priority Fees (Optional) - -```yaml -priority_fees: - enabled: true # Enable priority fees - fixed_amount: 10000 # Fixed fee amount in lamports - extra_percentage: 0.1 # Extra percentage of transaction cost (0.1 = 10%) - hard_cap: 100000 # Maximum fee cap in lamports -``` - -### Error Handling and Retries (Optional) - -```yaml -retries: - max_attempts: 3 # Maximum retry attempts - backoff_factor: 1.5 # Backoff multiplier between retries - initial_wait: 1 # Initial wait time in seconds -``` - -### Cleanup Settings (Optional) - -```yaml -cleanup: - mode: "after_sell" # Options: "disabled", "on_fail", "after_sell", "post_session" - keep_min_balance: true # Keep minimum balance in accounts -``` - -## Validation Rules - -- Numeric values must be within specified ranges (e.g., slippage between 0-1) -- Required fields must be present and correctly formatted -- Enum-like fields must use valid values from predefined options - -## Examples - -See sample configurations in the `bots/` directory: -- @bot-sniper-1-geyser.yaml: Fast trading using Geyser stream -- @bot-sniper-2-logs.yaml: Trading based on log monitoring -- @bot-sniper-3-blocks.yaml: Trading based on block monitoring -- @bot-sniper-4-pp.yaml: Trading using PumpPortal integration - diff --git a/.cursor/rules/cleanup.mdc b/.cursor/rules/cleanup.mdc deleted file mode 100644 index 1356560..0000000 --- a/.cursor/rules/cleanup.mdc +++ /dev/null @@ -1,68 +0,0 @@ ---- -description: Comprehensive guide for account cleanup operations and resource management. Apply when working with token account cleanup operations. -globs: src/cleanup/*.py -alwaysApply: true ---- - -# Account Cleanup System Guide - -## Overview - -The cleanup module handles the safe management and disposal of Solana token accounts after trading operations. This system ensures resources are properly released and minimizes unnecessary account maintenance fees. - -## Core Components - -### AccountCleanupManager - -@src/cleanup/manager.py implements the `AccountCleanupManager` class which: -- Handles safe cleanup of Associated Token Accounts (ATAs) -- Manages token burning operations when needed -- Implements proper account closure to recover rent -- Integrates with priority fee systems for high-congestion scenarios - -```python -# Example usage -cleanup_manager = AccountCleanupManager(client, wallet, priority_fee_manager, use_priority_fees) -await cleanup_manager.cleanup_ata(token_mint) -``` - -### Cleanup Modes - -@src/cleanup/modes.py defines various cleanup strategies and their implementation: - -1. **Disabled** - No automatic cleanup performed -2. **On Failure** - Clean up accounts only after transaction failures -3. **After Sell** - Clean up accounts after successful sell operations -4. **Post-Session** - Clean up all empty accounts when a trading session ends - -```python -# Example implementation -if should_cleanup_after_sell(cleanup_mode): - await manager.cleanup_ata(mint) -``` - -## Configuration - -Account cleanup is configured in bot YAML files: - -```yaml -cleanup: - mode: "post_session" # Options: "disabled", "on_fail", "after_sell", "post_session" - force_close_with_burn: false # Force burn remaining tokens before closing - with_priority_fee: false # Use priority fees for cleanup transactions -``` - -## When to Use Each Mode - -| Mode | Use Case | -|------|----------| -| `disabled` | Development/testing or when manual account management is preferred | -| `on_fail` | Conservative approach to only clean up failed transaction remnants | -| `after_sell` | Balance between efficiency and account reuse | -| `post_session` | Maximum cleanup, recommended for production | - -## Related Files - -- @src/cleanup/manager.py: Core account cleanup implementation -- @src/cleanup/modes.py: Cleanup mode strategies and handlers -- @src/core/client.py: Solana client interface used by cleanup operations diff --git a/.cursor/rules/core.mdc b/.cursor/rules/core.mdc deleted file mode 100644 index 31d62c5..0000000 --- a/.cursor/rules/core.mdc +++ /dev/null @@ -1,76 +0,0 @@ ---- -description: Core blockchain interaction components and Solana protocol abstractions. Apply when working with blockchain operations, transactions, or protocol-specific logic. -globs: src/core/*.py -alwaysApply: true ---- - -# Core Blockchain Components Guide - -## Overview - -The core module provides essential abstractions and utilities for interacting with the Solana blockchain, managing wallets, handling transactions, and working with protocol-specific logic such as bonding curves and token operations. - -## Key Components - -### Solana Client - -@src/core/client.py implements the `SolanaClient` class which: -- Handles RPC communication with Solana nodes -- Manages transaction submission and confirmation -- Provides methods for account data retrieval and subscription -- Implements retry logic and error handling for blockchain operations - -```python -# Example usage -client = SolanaClient(rpc_endpoint, wss_endpoint) -await client.get_token_account_data(token_account) -``` - -### Wallet Management - -@src/core/wallet.py provides wallet functionality: -- Secure private key handling -- Transaction signing -- Account derivation and management -- Integration with hardware wallets (where applicable) - -### Public Key Management - -@src/core/pubkeys.py contains: -- System address constants -- Program ID references -- Address derivation utilities -- Token account resolution functions - -### Bonding Curve Mathematics - -@src/core/curve.py offers: -- Mathematical models for bonding curve calculations -- Price impact estimation -- Liquidity modeling -- Swap pricing utilities - -### Priority Fee Management - -@src/core/priority_fee/manager.py implements: -- Dynamic priority fee calculation -- Fee cap enforcement -- Network congestion detection -- Transaction prioritization strategies - -## Common Operations - -| Operation | Key Functions | -|-----------|--------------| -| Send Transaction | `client.send_transaction(tx, opts)` | -| Get Account Data | `client.get_account_info(pubkey)` | -| Sign Message | `wallet.sign_message(message)` | -| Calculate Price | `curve.calculate_price_impact(amount)` | - -## Related Files - -- @src/core/client.py: Solana RPC client implementation -- @src/core/wallet.py: Wallet and signing functionality -- @src/core/pubkeys.py: Address management and derivation -- @src/core/curve.py: Bonding curve and pricing mathematics -- @src/core/priority_fee/manager.py: Transaction fee optimization diff --git a/.cursor/rules/idl.mdc b/.cursor/rules/idl.mdc deleted file mode 100644 index 93429e6..0000000 --- a/.cursor/rules/idl.mdc +++ /dev/null @@ -1,89 +0,0 @@ ---- -description: Program interface definitions for on-chain contract interactions. Apply when working with smart contract integration or program data structures. -globs: idl/*.json -alwaysApply: true ---- - -# Interface Definition Guide - -## Overview - -The IDL (Interface Definition Language) files define the structure and interface of on-chain Solana programs. These definitions enable the bot to interact with smart contracts, serialize/deserialize data, and interpret on-chain events correctly. - -## Core IDL Files - -### Pump Fun IDL - -@idl/pump_fun_idl.json contains the core protocol definitions: -- Main pump.fun protocol instruction schemas -- Account structures and data layouts -- Token creation and management instructions -- Event definitions and discriminators - -### Pump Swap IDL - -@idl/pump_swap_idl.json defines the swap interface: -- Token swap instruction formats -- Liquidity management operations -- Price calculation functions -- Fee structure definitions - -### Raydium AMM IDL - -@idl/raydium_amm_idl.json provides integration with Raydium: -- AMM pool interface definitions -- Liquidity provider instructions -- Market making operations -- Price discovery mechanisms - -## Working with IDLs - -IDL files are used throughout the codebase for: - -```python -# Example: Using IDLs for instruction building -with open("idl/pump_fun_idl.json", "r") as f: - pump_idl = json.load(f) - -# Extract instruction discriminator -discriminator = bytes(pump_idl["instructions"][0]["discriminator"]) -``` - -## IDL Structure - -Each IDL file follows this structure: - -```json -{ - "address": "Program address on chain", - "metadata": { - "name": "Program name", - "version": "Version number" - }, - "instructions": [ - { - "name": "instructionName", - "discriminator": [byte array], - "accounts": [...], - "args": [...] - } - ], - "accounts": [...], - "events": [...] -} -``` - -## IDL Usage - -| Component | Usage | -|-----------|-------| -| Instructions | Building transaction instructions | -| Discriminators | Identifying program events in logs | -| Accounts | Parsing account data | -| Events | Interpreting program-emitted events | - -## Related Files - -- @idl/pump_fun_idl.json: Main pump.fun protocol definition -- @idl/pump_swap_idl.json: Swap functionality interface -- @idl/raydium_amm_idl.json: Raydium AMM integration diff --git a/.cursor/rules/monitoring.mdc b/.cursor/rules/monitoring.mdc deleted file mode 100644 index c2e223c..0000000 --- a/.cursor/rules/monitoring.mdc +++ /dev/null @@ -1,96 +0,0 @@ ---- -description: Event monitoring and data collection systems for tracking blockchain activity. Apply when implementing token detection, event processing, or subscription logic. -globs: src/monitoring/*.py -alwaysApply: true ---- - -# Monitoring System Guide - -## Overview - -The monitoring module provides various mechanisms to detect and process events from the Solana blockchain. It offers multiple strategies for token discovery, event filtering, and real-time notifications through different data sources. - -## Listener Architecture - -### Base Listener - -@src/monitoring/base_listener.py defines the abstract base class `BaseTokenListener` which: -- Provides a common interface for all token listeners -- Declares required methods for event subscription -- Standardizes token event processing - -```python -# Common listener interface -async def listen_for_tokens( - self, - token_callback: Callable[[TokenInfo], Awaitable[None]], - match_string: str | None = None, - creator_address: str | None = None, -) -> None: - pass -``` - -### Listener Implementations - -#### Geyser Listener (Fastest) - -@src/monitoring/geyser_listener.py implements the Geyser-based listener: -- Direct connection to Geyser WebSocket endpoint -- Lowest latency for token detection -- Advanced filtering capabilities - -#### Logs Listener - -@src/monitoring/logs_listener.py provides log-based monitoring: -- Subscription to program logs -- Pattern matching for token creation events -- Program-specific event detection - -#### Block Listener - -@src/monitoring/block_listener.py implements block-based scanning: -- Processes entire blocks -- Extracts transaction data -- Identifies token creation events - -#### PumpPortal Listener - -@src/monitoring/pumpportal_listener.py offers integration with external service: -- Connects to PumpPortal API -- Receives preprocessed token events -- Simplifies integration with third-party data providers - -## Event Processing - -Each listener has a corresponding event processor that: -- Validates incoming events -- Extracts relevant token information -- Applies filtering rules -- Dispatches events to trading components - -## Configuration - -In bot configuration files, specify the listener type: - -```yaml -filters: - listener_type: "geyser" # Options: "logs", "blocks", "geyser", "pumpportal" - max_token_age: 0.001 # Maximum token age in seconds -``` - -## Comparison of Approaches - -| Listener Type | Latency | CPU Usage | Reliability | Use Case | -|---------------|---------|-----------|-------------|----------| -| Geyser | Lowest | Medium | High | Production sniper bots | -| Logs | Low | Low | High | General purpose | -| Blocks | Medium | High | Very High | Deep scanning | -| PumpPortal | Varies | Very Low | Depends | Easy integration | - -## Related Files - -- @src/monitoring/base_listener.py: Abstract listener interface -- @src/monitoring/geyser_listener.py: Geyser-based implementation -- @src/monitoring/logs_listener.py: Program log monitoring -- @src/monitoring/block_listener.py: Block processing implementation -- @src/monitoring/pumpportal_listener.py: External API integration diff --git a/.cursor/rules/package-management.mdc b/.cursor/rules/package-management.mdc deleted file mode 100644 index 5ddc037..0000000 --- a/.cursor/rules/package-management.mdc +++ /dev/null @@ -1,89 +0,0 @@ ---- -description: Package management and command execution guidelines using uv. Apply when installing dependencies, running scripts, or managing the Python environment. -globs: "**/*" -alwaysApply: true ---- - -# Package Management with UV - -## Overview - -This project exclusively uses `uv` as the package manager for all Python operations. UV is a fast Python package installer and resolver, written in Rust, that serves as a drop-in replacement for pip and other Python package managers. - -## Core Principles - -1. **Use `uv` for all package operations** - Never use pip, pipenv, poetry, or conda -2. **Use `uv run` for script execution** - Ensures proper virtual environment activation -3. **Maintain consistency** - All documentation and examples should reference uv commands - -## Package Installation - -### Installing Dependencies - -```bash -# Install all project dependencies -uv sync --extra dev - -# Install additional packages -uv add package-name - -# Install from requirements file -uv add -r requirements.txt -``` - -### Managing Virtual Environments - -```bash -# Create virtual environment -uv venv - -# Activate virtual environment (if needed manually) -source .venv/bin/activate -``` - -## Script Execution - -### Running Bot Scripts - -```bash -# Primary method: Use the installed command -pump_bot - -# Alternative: Run bot_runner.py directly -uv run src/bot_runner.py -``` - -### Running Test Scripts - -```bash -# Run listener performance tests -uv run tests/test_geyser_listener.py -uv run tests/test_logs_listener.py -uv run tests/test_block_listener.py - -# Run comparative analysis -uv run tests/compare_listeners.py -uv run tests/compare_listeners.py 60 -``` - -### Development and Testing - -```bash -# Run linting -uv run ruff check src/ - -# Run formatting -uv run ruff format src/ -``` - -## Project Structure Integration - -The project is configured to work seamlessly with uv: - -- `pyproject.toml` - Defines project dependencies and the `pump_bot` command -- `uv.lock` - Locks dependency versions for reproducible builds -- `.venv/` - Virtual environment managed by uv - -## Environment Configuration - -UV automatically detects and uses the virtual environment when running commands. \ No newline at end of file diff --git a/.cursor/rules/project-structure.mdc b/.cursor/rules/project-structure.mdc deleted file mode 100644 index 23b761c..0000000 --- a/.cursor/rules/project-structure.mdc +++ /dev/null @@ -1,115 +0,0 @@ ---- -description: Comprehensive overview of the project architecture and organization. Apply when working with the codebase structure or integrating new components. -globs: *.md -alwaysApply: true ---- - -# Project Structure Guide - -## Overview - -The pump-bot system is organized into modular components with clear separation of concerns. This architecture facilitates maintainability, testing, and extension of functionality. - -## Directory Structure - -### Source Code - -The main codebase is organized as follows: - -- @src/: Root source directory - - @src/bot_runner.py: Entry point for bot execution - - @src/config_loader.py: Configuration loading and validation - -### Package Command - -The project provides a convenient command-line interface: -- `pump_bot`: Installed command that runs all enabled bots (defined in @pyproject.toml) -- Alternative: `uv run src/bot_runner.py` for direct script execution - -#### Core Components - -- @src/core/: Blockchain interaction and protocol abstractions - - @src/core/client.py: Solana client implementation - - @src/core/wallet.py: Wallet management and transaction signing - - @src/core/pubkeys.py: System and program addresses - - @src/core/curve.py: Bonding curve mathematics - - @src/core/priority_fee/: Priority fee optimization system - -#### Trading System - -- @src/trading/: Trading execution and strategy - - @src/trading/trader.py: Main trading coordination - - @src/trading/buyer.py: Token purchase implementation - - @src/trading/seller.py: Token sale implementation - - @src/trading/position.py: Position tracking and management - - @src/trading/base.py: Shared abstractions - -#### Monitoring System - -- @src/monitoring/: Event detection and processing - - @src/monitoring/geyser_listener.py: Geyser-based monitoring - - @src/monitoring/logs_listener.py: Log-based monitoring - - @src/monitoring/block_listener.py: Block-based monitoring - - @src/monitoring/pumpportal_listener.py: PumpPortal integration - - @src/monitoring/base_listener.py: Shared listener abstractions - -#### Resource Management - -- @src/cleanup/: Token account management - - @src/cleanup/manager.py: Account cleanup operations - - @src/cleanup/modes.py: Cleanup strategy implementation - -#### Utilities - -- @src/utils/: Shared utilities - - @src/utils/logger.py: Logging framework - - @src/utils/env.py: Environment configuration - -### Configuration - -- @bots/: Bot configuration files - - @bots/bot-sniper-1-geyser.yaml: Geyser-based sniper configuration - - @bots/bot-sniper-2-logs.yaml: Log-based sniper configuration - - @bots/bot-sniper-3-blocks.yaml: Block-based sniper configuration - - @bots/bot-sniper-4-pp.yaml: PumpPortal sniper configuration - -### Program Interfaces - -- @idl/: Interface definitions for on-chain programs - - @idl/pump_fun_idl.json: Pump.fun protocol interface - - @idl/pump_swap_idl.json: Swap interface definition - - @idl/raydium_amm_idl.json: Raydium AMM interface - -### Testing - -- @tests/: Test cases and validation - - @tests/compare_listeners.py: Listener comparison framework - - @tests/test_geyser_listener.py: Geyser listener tests - - @tests/test_logs_listener.py: Logs listener tests - - @tests/test_block_listener.py: Block listener tests - -### Operational Data - -- @logs/: Application logs -- @trades/: Trading activity records - -## Development Flow - -1. Configure bot parameters in @bots/*.yaml -2. Launch bot using `uv run pump_bot` or `uv run src/bot_runner.py` -3. Monitor execution in @logs/ -4. Review trade outcomes in @trades/trades.log - -## Architecture Principles - -1. **Modular Design**: Components have clear responsibilities and interfaces -2. **Configurability**: External configuration for adjustable behavior -3. **Extensibility**: Easy to add new listeners or trading strategies -4. **Reliability**: Error handling and recovery throughout the system -5. **Observability**: Comprehensive logging and monitoring - -## Related Documents - -- @README.md: Project overview and setup instructions -- @MAINTAINERS.md: Maintenance guidelines and procedures -- @CLAUDE.md: Development rules for Claude Code diff --git a/.cursor/rules/tests.mdc b/.cursor/rules/tests.mdc deleted file mode 100644 index fcd7054..0000000 --- a/.cursor/rules/tests.mdc +++ /dev/null @@ -1,153 +0,0 @@ ---- -description: Performance testing and listener comparison scripts for token detection systems. Apply when benchmarking listener performance or validating detection accuracy. -globs: tests/*.py -alwaysApply: true ---- - -# Listener Performance Testing Guide - -## Overview - -The test scripts provide performance comparison and validation tools for different listener implementations. These are standalone scripts designed to measure which listener is fastest at discovering new tokens, helping optimize the bot's detection capabilities. - -## Individual Listener Tests - -Each listener has a dedicated test script for standalone performance analysis: - -### Geyser Listener Test - -@tests/test_geyser_listener.py tests the fastest detection method: -- Direct gRPC connection to Geyser endpoint -- Real-time token creation monitoring -- Performance measurement and token logging - -```bash -# Run Geyser listener test for 30 seconds -uv run tests/test_geyser_listener.py -``` - -### Logs Listener Test - -@tests/test_logs_listener.py validates log-based monitoring: -- WebSocket subscription to program logs -- Pattern matching for token creation events -- Detection accuracy verification - -```bash -# Run logs listener test -uv run tests/test_logs_listener.py -``` - -### Block Listener Test - -@tests/test_block_listener.py tests block-based scanning: -- Full block processing for token discovery -- Transaction analysis and extraction -- Comprehensive but potentially slower detection - -```bash -# Run block listener test -uv run tests/test_block_listener.py -``` - -## Comparative Performance Analysis - -### Listener Comparison Tool - -@tests/compare_listeners.py runs all listeners simultaneously: -- Side-by-side performance measurement -- Detection speed comparison with timing data -- Accuracy analysis (tokens detected by each method) -- Statistical reporting on detection overlap - -```bash -# Compare all listeners for 60 seconds -uv run tests/compare_listeners.py 60 - -# Default 60-second comparison -uv run tests/compare_listeners.py -``` - -### Performance Metrics - -The comparison tool provides: - -| Metric | Description | -|--------|-------------| -| Detection Speed | Time to detect each token (microsecond precision) | -| Detection Count | Total tokens found by each listener | -| Overlap Analysis | Tokens detected by multiple listeners | -| Unique Detections | Tokens found only by specific listeners | - -## Test Output Analysis - -### Individual Test Results - -Each listener test displays: -``` -================================================== -NEW TOKEN: TokenName -Symbol: SYMBOL -Mint: TokenMintAddress -URI: MetadataURI -Creator: CreatorAddress -================================================== -``` - -### Comparison Results - -The comparison tool generates: -``` -Geyser detected: 15 tokens -Logs detected: 12 tokens -Block detected: 18 tokens - -Token | Geyser | Logs | Block | Fastest -------|--------|------|-------|-------- -ABC123... | 1.234567 | N/A | 2.345678 | Geyser -DEF456... | 0.987654 | 1.123456 | 1.567890 | Geyser -``` - -## Environment Configuration - -Tests require these environment variables: -```bash -export SOLANA_NODE_RPC_ENDPOINT="https://api.mainnet-beta.solana.com" -export SOLANA_NODE_WSS_ENDPOINT="wss://api.mainnet-beta.solana.com" -export GEYSER_ENDPOINT="wss://your-geyser-endpoint" -export GEYSER_API_TOKEN="your-api-token" -``` - -## Common Usage Patterns - -### Quick Performance Check -```bash -# 30-second comparison of all listeners -uv run tests/compare_listeners.py 30 -``` - -### Extended Performance Analysis -```bash -# 10-minute deep analysis -uv run tests/compare_listeners.py 600 -``` - -### Individual Listener Validation -```bash -# Test specific listener implementation -uv run tests/test_geyser_listener.py -``` - -## Use Cases - -1. **Performance Optimization**: Determine fastest listener for production -2. **Reliability Testing**: Verify consistent token detection -3. **Configuration Tuning**: Optimize listener parameters -4. **Debugging**: Isolate listener-specific issues - -## Related Files - -- @tests/test_geyser_listener.py: Geyser performance testing -- @tests/test_logs_listener.py: Logs listener validation -- @tests/test_block_listener.py: Block listener analysis -- @tests/compare_listeners.py: Multi-listener performance comparison diff --git a/.cursor/rules/trading.mdc b/.cursor/rules/trading.mdc deleted file mode 100644 index 549ab46..0000000 --- a/.cursor/rules/trading.mdc +++ /dev/null @@ -1,92 +0,0 @@ ---- -description: Core trading functionality including buy/sell operations, position management, and execution strategies. Apply when working with trading logic or transaction execution. -globs: src/trading/*.py -alwaysApply: true ---- - -# Trading System Guide - -## Overview - -The trading module forms the core of the pump-bot's execution capabilities, handling token purchases, sales, position management, and trading strategy implementation. This system translates token detection events into actual on-chain transactions. - -## Core Components - -### Trading Coordinator - -@src/trading/trader.py implements the `PumpTrader` class which: -- Orchestrates the entire trading lifecycle -- Manages token detection to execution flow -- Implements risk management controls -- Coordinates buy and sell operations -- Handles position tracking and reporting - -```python -# Example usage -trader = PumpTrader(config, client, wallet) -await trader.process_token(token_info) -``` - -### Buy Operations - -@src/trading/buyer.py provides the `TokenBuyer` class: -- Implements token purchase logic -- Handles slippage calculation and management -- Creates and submits buy transactions -- Validates transaction success - -### Sell Operations - -@src/trading/seller.py implements the `TokenSeller` class: -- Manages token selling operations -- Implements various exit strategies -- Handles profit calculation and optimization -- Ensures proper transaction construction - -### Position Management - -@src/trading/position.py offers position tracking: -- Records entry and exit prices -- Calculates profit/loss metrics -- Manages position state throughout lifecycle -- Implements risk management boundaries - -### Base Abstractions - -@src/trading/base.py defines core abstractions: -- Common interfaces for trading components -- Shared data structures -- Type definitions and constants -- Interface contracts for strategy implementations - -## Trading Strategies - -The system supports multiple exit strategies: - -1. **Time-Based**: Automatically sells after a configurable holding period -2. **TP/SL**: Uses Take Profit / Stop Loss targets to manage exits -3. **Manual**: Requires explicit sell commands - -```yaml -# Configuration example -trade: - exit_strategy: "tp_sl" - take_profit_percentage: 0.2 - stop_loss_percentage: 0.2 -``` - -## Common Trading Flows - -| Operation | Flow | -|-----------|------| -| Token Purchase | Detection → Validation → Buy Transaction → Position Creation | -| Token Sale | Strategy Trigger → Position Update → Sell Transaction → Cleanup | -| Error Handling | Error Detection → Transaction Retry → Position Closure → Cleanup | - -## Related Files - -- @src/trading/trader.py: Main trading coordination -- @src/trading/buyer.py: Buy transaction implementation -- @src/trading/seller.py: Sell transaction implementation -- @src/trading/position.py: Position tracking and management -- @src/trading/base.py: Core abstractions and interfaces diff --git a/.cursor/rules/utils.mdc b/.cursor/rules/utils.mdc deleted file mode 100644 index 729c071..0000000 --- a/.cursor/rules/utils.mdc +++ /dev/null @@ -1,44 +0,0 @@ ---- -description: Shared utilities and helper functions for common operations. Apply when working with project-wide utilities. -globs: src/utils/*.py -alwaysApply: true ---- - -# Utility Functions Guide - -## Overview - -The utils module provides shared functionality and helper methods used throughout the pump-bot system. These utilities handle common concerns like logging. - -## Core Utilities - -### Logging - -@src/utils/logger.py implements a centralized logging system: -- Consistent log formatting across the application -- Log level management -- File and console output handling -- Context-aware logging with component identification - -```python -# Example usage -logger = get_logger(__name__) -logger.info("Processing token %s", token_mint) -``` - -## Common Patterns - -| Operation | Utility | -|-----------|---------| -| Logging | `get_logger(__name__)` | - -## Best Practices - -1. **Reuse existing utilities** rather than implementing similar functionality -2. **Keep utilities focused** on a single responsibility -3. **Document usage examples** for each utility -4. **Maintain backward compatibility** when enhancing utilities - -## Related Files - -- @src/utils/logger.py: Centralized logging system diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 91eb451..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,120 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Development Commands - -### Environment Setup -```bash -# Create virtual environment and install dependencies -uv sync --extra dev - -# Activate virtual environment -source .venv/bin/activate # Unix/macOS -.venv\Scripts\activate # Windows -``` - -### Running the Bot -```bash -# Run bot with all configurations in bots/ directory -pump_bot - -# Run bot directly -uv run src/bot_runner.py -``` - -### Development Tools -```bash -# Run linting -uv run ruff check src/ - -# Run formatting -uv run ruff format src/ -``` - -### Testing -```bash -# Run individual test files -uv run tests/test_block_listener.py -uv run tests/test_geyser_listener.py -uv run tests/test_logs_listener.py -``` - -## Architecture Overview - -This is a Solana-based trading bot for pump.fun tokens with a modular architecture: - -### Core Components - -- **`src/bot_runner.py`**: Main entry point that loads configurations and starts trading bots -- **`src/config_loader.py`**: Handles YAML configuration loading, validation, and environment variable resolution -- **`src/trading/trader.py`**: `PumpTrader` class - main trading coordinator that orchestrates token detection, buying, and selling -- **`src/core/client.py`**: `SolanaClient` - abstraction for Solana RPC operations with background blockhash caching -- **`src/core/wallet.py`**: Wallet management for signing transactions -- **`src/core/curve.py`**: Bonding curve calculations for pump.fun tokens - -### Trading System - -- **`src/trading/buyer.py`**: `TokenBuyer` - handles token purchase logic with slippage and retry support -- **`src/trading/seller.py`**: `TokenSeller` - handles token sale logic -- **`src/trading/position.py`**: `Position` - tracks positions with take-profit/stop-loss functionality -- **`src/trading/base.py`**: Common trading data structures (`TokenInfo`, `TradeResult`) - -### Token Detection (Monitoring) - -Multiple listener implementations for detecting new tokens: -- **`src/monitoring/logs_listener.py`**: Uses `logsSubscribe` WebSocket method -- **`src/monitoring/block_listener.py`**: Uses `blockSubscribe` WebSocket method -- **`src/monitoring/geyser_listener.py`**: Uses Geyser gRPC streaming (fastest) -- **`src/monitoring/pumpportal_listener.py`**: Uses PumpPortal WebSocket API -- **`src/monitoring/base_listener.py`**: Abstract base class for all listeners - -### Priority Fees - -- **`src/core/priority_fee/manager.py`**: Manages priority fee calculation -- **`src/core/priority_fee/dynamic_fee.py`**: Dynamic fee calculation based on network conditions -- **`src/core/priority_fee/fixed_fee.py`**: Fixed fee implementation - -### Configuration System - -Bot configurations are stored in `bots/*.yaml` files. Each bot can run independently with its own settings for: -- Connection endpoints (RPC, WSS, Geyser) -- Trading parameters (buy amount, slippage, exit strategy) -- Priority fees (dynamic vs fixed) -- Token filters (match string, creator address) -- Cleanup modes for token accounts - -### Key Features - -1. **Multiple Listener Types**: Supports logs, blocks, Geyser, and PumpPortal for token detection -2. **Exit Strategies**: Time-based, take-profit/stop-loss, or manual -3. **Extreme Fast Mode**: Skips stabilization wait and price checks for faster execution -4. **Priority Fee Management**: Dynamic or fixed fee calculation -5. **Account Cleanup**: Automatic cleanup of empty token accounts -6. **Multi-bot Support**: Run multiple bots with different configurations simultaneously - -### Trading Modes - -- **Single Token Mode** (`yolo_mode: false`): Process one token and exit -- **Continuous Mode** (`yolo_mode: true`): Continuously process tokens -- **Marry Mode** (`marry_mode: true`): Only buy tokens, skip selling - -## Environment Configuration - -Create `.env` file with: -``` -SOLANA_NODE_RPC_ENDPOINT=your_rpc_endpoint -SOLANA_NODE_WSS_ENDPOINT=your_wss_endpoint -SOLANA_PRIVATE_KEY=your_private_key -GEYSER_ENDPOINT=your_geyser_endpoint -GEYSER_API_TOKEN=your_geyser_token -``` - -## Important Notes - -- This is educational/learning code - not for production use -- The bot trades on pump.fun bonding curves and handles migrations to PumpSwap -- Geyser listener provides fastest token detection -- All configurations support environment variable substitution with `${VAR}` syntax -- Bot logs are stored in `logs/` directory with timestamps -- Trade logs are written to `trades/trades.log` \ No newline at end of file diff --git a/bots/bot-sniper-1-geyser.yaml b/bots/bot-sniper-1-geyser.yaml index 4fc13aa..ec05ef4 100644 --- a/bots/bot-sniper-1-geyser.yaml +++ b/bots/bot-sniper-1-geyser.yaml @@ -11,6 +11,9 @@ private_key: "${SOLANA_PRIVATE_KEY}" enabled: false # You can turn off the bot w/o removing its config separate_process: true +# Options: "pump_fun" (default), "lets_bonk" +platform: "pump_fun" + # Geyser configuration (fastest method for getting updates) geyser: endpoint: "${GEYSER_ENDPOINT}" diff --git a/bots/bot-sniper-2-logs.yaml b/bots/bot-sniper-2-logs.yaml index d9e2e1c..46562e5 100644 --- a/bots/bot-sniper-2-logs.yaml +++ b/bots/bot-sniper-2-logs.yaml @@ -11,6 +11,9 @@ private_key: "${SOLANA_PRIVATE_KEY}" enabled: true # You can turn off the bot w/o removing its config separate_process: true +# Options: "pump_fun" (default), "lets_bonk" +platform: "pump_fun" + # Geyser configuration (fastest method for getting updates) geyser: endpoint: "${GEYSER_ENDPOINT}" diff --git a/bots/bot-sniper-3-blocks.yaml b/bots/bot-sniper-3-blocks.yaml index 0774e6b..fe58ece 100644 --- a/bots/bot-sniper-3-blocks.yaml +++ b/bots/bot-sniper-3-blocks.yaml @@ -11,6 +11,9 @@ private_key: "${SOLANA_PRIVATE_KEY}" enabled: false # You can turn off the bot w/o removing its config separate_process: true +# Options: "pump_fun" (default), "lets_bonk" +platform: "pump_fun" + # Geyser configuration (fastest method for getting updates) geyser: endpoint: "${GEYSER_ENDPOINT}" diff --git a/bots/bot-sniper-4-pp.yaml b/bots/bot-sniper-4-pp.yaml index 2ef9235..4e8df34 100644 --- a/bots/bot-sniper-4-pp.yaml +++ b/bots/bot-sniper-4-pp.yaml @@ -11,6 +11,9 @@ private_key: "${SOLANA_PRIVATE_KEY}" enabled: false # You can turn off the bot w/o removing its config separate_process: true +# Options: "pump_fun" (default), "lets_bonk" +platform: "pump_fun" + # PumpPortal configuration (optional - uses default URL if not specified) pumpportal: url: "wss://pumpportal.fun/api/data" # Default PumpPortal WebSocket URL diff --git a/src/config_loader.py b/src/config_loader.py index 4d8b2d2..79e3c47 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -1,9 +1,19 @@ +""" +Updated configuration validation with platform support. + +This extends the existing config_loader.py to support platform selection +and validation while maintaining backward compatibility. +""" + import os from typing import Any import yaml from dotenv import load_dotenv +from interfaces.core import Platform + +# Existing validation rules (keeping all existing ones) REQUIRED_FIELDS = [ "name", "rpc_endpoint", @@ -64,17 +74,19 @@ CONFIG_VALIDATION_RULES = [ ), ] -# Valid values for enum-like fields +# Valid values for enum-like fields (extended with platform support) VALID_VALUES = { "filters.listener_type": ["logs", "blocks", "geyser", "pumpportal"], "cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"], "trade.exit_strategy": ["time_based", "tp_sl", "manual"], + "platform": ["pump_fun", "lets_bonk"], # New platform validation } def load_bot_config(path: str) -> dict: """ Load and validate a bot configuration from a YAML file. + Extended to support platform selection with backward compatibility. Args: path: Path to the YAML configuration file (relative or absolute) @@ -99,6 +111,11 @@ 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 @@ -157,6 +174,7 @@ def get_nested_value(config: dict, path: str) -> Any: def validate_config(config: dict) -> None: """ Validate the configuration against defined rules. + Extended to include platform-specific validation. Args: config: Configuration dictionary to validate @@ -164,9 +182,11 @@ def validate_config(config: dict) -> None: Raises: ValueError: If the configuration is invalid """ + # Validate required fields for field in REQUIRED_FIELDS: get_nested_value(config, field) + # Validate config rules for path, expected_type, min_val, max_val, error_msg in CONFIG_VALIDATION_RULES: try: value = get_nested_value(config, path) @@ -206,15 +226,70 @@ def validate_config(config: dict) -> None: # Skip if one of the fields is missing pass + # Platform-specific validation + platform_str = config.get("platform", "pump_fun") + try: + platform = Platform(platform_str) + 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 + + +def validate_platform_config(config: dict, platform: Platform) -> None: + """ + Validate platform-specific configuration requirements. + + Args: + config: Configuration dictionary + platform: Platform enum value + + Raises: + ValueError: If platform-specific config is invalid + """ + 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 + pass + + +def get_platform_from_config(config: dict) -> Platform: + """ + Extract platform enum from configuration. + + Args: + config: Configuration dictionary + + Returns: + Platform enum value + + Raises: + ValueError: If platform is invalid + """ + platform_str = config.get("platform", "pump_fun") + try: + return Platform(platform_str) + except ValueError: + raise ValueError(f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}") + def print_config_summary(config: dict) -> None: """ Print a summary of the loaded configuration. + Extended to show platform information. Args: config: Configuration dictionary """ + 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')}" ) @@ -240,5 +315,9 @@ def print_config_summary(config: dict) -> None: if __name__ == "__main__": + # Example usage with platform configuration config = load_bot_config("bots/bot-sniper.yaml") print_config_summary(config) + + platform = get_platform_from_config(config) + print(f"Detected platform: {platform}") \ No newline at end of file diff --git a/src/interfaces/__init__.py b/src/interfaces/__init__.py new file mode 100644 index 0000000..c2b3dbf --- /dev/null +++ b/src/interfaces/__init__.py @@ -0,0 +1,280 @@ +""" +Platform factory and registry for managing multiple trading platforms. + +This module provides a centralized way to instantiate and access +platform-specific implementations of the trading interfaces. +""" + +from dataclasses import dataclass +from typing import Any, Dict, Optional, Type + +from core.client import SolanaClient +from interfaces.core import ( + AddressProvider, + CurveManager, + EventParser, + InstructionBuilder, + Platform, +) + + +@dataclass +class PlatformImplementations: + """Container for all platform-specific implementations.""" + address_provider: AddressProvider + instruction_builder: InstructionBuilder + curve_manager: CurveManager + event_parser: EventParser + + +class PlatformRegistry: + """Registry for platform implementations.""" + + def __init__(self): + self._implementations: dict[Platform, dict[str, type]] = {} + self._instances: dict[Platform, 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] + ) -> None: + """Register platform implementations. + + Args: + platform: Platform enum value + address_provider_class: AddressProvider implementation class + instruction_builder_class: InstructionBuilder implementation class + curve_manager_class: CurveManager implementation class + 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 + } + + def create_platform_implementations( + self, + platform: Platform, + client: SolanaClient, + **kwargs: Any + ) -> PlatformImplementations: + """Create platform implementation instances. + + 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") + + # Check if we already have instances for this platform + if platform in self._instances: + return self._instances[platform] + + impl_classes = self._implementations[platform] + + # Create instances + address_provider = impl_classes['address_provider']() + 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 + ) + + # Cache the instances + self._instances[platform] = implementations + + return implementations + + def get_platform_implementations(self, platform: Platform) -> PlatformImplementations | None: + """Get cached platform implementations. + + Args: + platform: Platform to get implementations for + + Returns: + PlatformImplementations if available, None otherwise + """ + return self._instances.get(platform) + + 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 + + +class PlatformFactory: + """Factory for creating platform-specific implementations.""" + + def __init__(self): + self.registry = PlatformRegistry() + self._setup_default_platforms() + + def _setup_default_platforms(self) -> None: + """Setup default platform registrations. + + This will be populated as we implement each platform. + """ + # Platforms will be registered here as they are implemented + # Example: + # from platforms.pumpfun import PumpFunAddressProvider, PumpFunInstructionBuilder, ... + # self.registry.register_platform( + # Platform.PUMP_FUN, + # PumpFunAddressProvider, + # PumpFunInstructionBuilder, + # PumpFunCurveManager, + # PumpFunEventParser + # ) + pass + + def create_for_platform( + 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 + + 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: + """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) + return implementations.address_provider + + 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) + return implementations.instruction_builder + + 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) + 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) + 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() + + +# Global factory instance +platform_factory = PlatformFactory() + + +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 + """ + return platform_factory.create_for_platform(platform, client) + + +def register_platform_implementations( + platform: Platform, + address_provider_class: type[AddressProvider], + instruction_builder_class: type[InstructionBuilder], + curve_manager_class: type[CurveManager], + event_parser_class: type[EventParser] +) -> None: + """Register platform implementations with the global factory. + + Args: + platform: Platform enum value + address_provider_class: AddressProvider implementation class + instruction_builder_class: InstructionBuilder implementation class + curve_manager_class: CurveManager implementation class + event_parser_class: EventParser implementation class + """ + platform_factory.registry.register_platform( + platform, + address_provider_class, + instruction_builder_class, + curve_manager_class, + event_parser_class + ) \ No newline at end of file diff --git a/src/interfaces/core.py b/src/interfaces/core.py new file mode 100644 index 0000000..7ad5a6d --- /dev/null +++ b/src/interfaces/core.py @@ -0,0 +1,363 @@ +""" +Core interfaces for multi-platform trading bot architecture. + +This module defines the abstract base classes that each trading platform +must implement to enable unified trading operations across different protocols. +""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from enum import Enum +from typing import Any + +from solders.instruction import Instruction +from solders.pubkey import Pubkey + + +class Platform(Enum): + """Supported trading platforms.""" + PUMP_FUN = "pump_fun" + LETS_BONK = "lets_bonk" + + +@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 + associated_bonding_curve: Pubkey | None = None # pump.fun specific + 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 + + +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: + """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 + """ + pass + + +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, + token_info: TokenInfo, + user: Pubkey, + amount_in: int, + minimum_amount_out: int, + 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, + token_info: TokenInfo, + user: Pubkey, + amount_in: int, + minimum_amount_out: int, + 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 + ) -> 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 + ) -> 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 + """ + pass + + +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 + ) -> 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 + ) -> 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) + """ + pass + + +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 + ) -> 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] + ) -> 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 + ) -> 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 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 diff --git a/src/platforms/__init__.py b/src/platforms/__init__.py new file mode 100644 index 0000000..ea584fb --- /dev/null +++ b/src/platforms/__init__.py @@ -0,0 +1,282 @@ +""" +Platform factory and registry for managing multiple trading platforms. + +This module provides a centralized way to instantiate and access +platform-specific implementations of the trading interfaces. +""" + +from dataclasses import dataclass +from typing import Any, Dict, Optional, Type + +from core.client import SolanaClient +from interfaces.core import ( + AddressProvider, + CurveManager, + EventParser, + InstructionBuilder, + Platform, +) + +from . import letsbonk, pumpfun + + +@dataclass +class PlatformImplementations: + """Container for all platform-specific implementations.""" + address_provider: AddressProvider + instruction_builder: InstructionBuilder + curve_manager: CurveManager + event_parser: EventParser + + +class PlatformRegistry: + """Registry for platform implementations.""" + + def __init__(self): + self._implementations: dict[Platform, dict[str, type]] = {} + self._instances: dict[Platform, 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] + ) -> None: + """Register platform implementations. + + Args: + platform: Platform enum value + address_provider_class: AddressProvider implementation class + instruction_builder_class: InstructionBuilder implementation class + curve_manager_class: CurveManager implementation class + 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 + } + + def create_platform_implementations( + self, + platform: Platform, + client: SolanaClient, + **kwargs: Any + ) -> PlatformImplementations: + """Create platform implementation instances. + + 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") + + # Check if we already have instances for this platform + if platform in self._instances: + return self._instances[platform] + + impl_classes = self._implementations[platform] + + # Create instances + address_provider = impl_classes['address_provider']() + 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 + ) + + # Cache the instances + self._instances[platform] = implementations + + return implementations + + def get_platform_implementations(self, platform: Platform) -> PlatformImplementations | None: + """Get cached platform implementations. + + Args: + platform: Platform to get implementations for + + Returns: + PlatformImplementations if available, None otherwise + """ + return self._instances.get(platform) + + 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 + + +class PlatformFactory: + """Factory for creating platform-specific implementations.""" + + def __init__(self): + self.registry = PlatformRegistry() + self._setup_default_platforms() + + def _setup_default_platforms(self) -> None: + """Setup default platform registrations. + + This will be populated as we implement each platform. + """ + # Platforms will be registered here as they are implemented + # Example: + # from platforms.pumpfun import PumpFunAddressProvider, PumpFunInstructionBuilder, ... + # self.registry.register_platform( + # Platform.PUMP_FUN, + # PumpFunAddressProvider, + # PumpFunInstructionBuilder, + # PumpFunCurveManager, + # PumpFunEventParser + # ) + pass + + def create_for_platform( + 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 + + 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: + """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) + return implementations.address_provider + + 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) + return implementations.instruction_builder + + 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) + 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) + 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() + + +# Global factory instance +platform_factory = PlatformFactory() + + +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 + """ + return platform_factory.create_for_platform(platform, client) + + +def register_platform_implementations( + platform: Platform, + address_provider_class: type[AddressProvider], + instruction_builder_class: type[InstructionBuilder], + curve_manager_class: type[CurveManager], + event_parser_class: type[EventParser] +) -> None: + """Register platform implementations with the global factory. + + Args: + platform: Platform enum value + address_provider_class: AddressProvider implementation class + instruction_builder_class: InstructionBuilder implementation class + curve_manager_class: CurveManager implementation class + event_parser_class: EventParser implementation class + """ + platform_factory.registry.register_platform( + platform, + address_provider_class, + instruction_builder_class, + curve_manager_class, + event_parser_class + ) \ No newline at end of file diff --git a/src/platforms/letsbonk/__init__.py b/src/platforms/letsbonk/__init__.py new file mode 100644 index 0000000..8e494e8 --- /dev/null +++ b/src/platforms/letsbonk/__init__.py @@ -0,0 +1,31 @@ +""" +LetsBonk platform registration and exports. + +This module registers all LetsBonk implementations with the platform factory +and provides convenient imports for the LetsBonk platform. +""" + +from interfaces.core import Platform +from platforms import register_platform_implementations + +from .address_provider import LetsBonkAddressProvider +from .curve_manager import LetsBonkCurveManager +from .event_parser import LetsBonkEventParser +from .instruction_builder import LetsBonkInstructionBuilder + +# Register LetsBonk platform implementations +register_platform_implementations( + Platform.LETS_BONK, + LetsBonkAddressProvider, + LetsBonkInstructionBuilder, + LetsBonkCurveManager, + LetsBonkEventParser +) + +# Export implementations for direct use if needed +__all__ = [ + 'LetsBonkAddressProvider', + 'LetsBonkCurveManager', + 'LetsBonkEventParser', + 'LetsBonkInstructionBuilder' +] \ No newline at end of file diff --git a/src/platforms/letsbonk/address_provider.py b/src/platforms/letsbonk/address_provider.py new file mode 100644 index 0000000..4877c13 --- /dev/null +++ b/src/platforms/letsbonk/address_provider.py @@ -0,0 +1,244 @@ +""" +LetsBonk implementation of AddressProvider interface. + +This module provides all LetsBonk (Raydium LaunchLab) specific addresses and PDA derivations +by implementing the AddressProvider interface. +""" + + +from solders.pubkey import Pubkey +from spl.token.instructions import get_associated_token_address + +from interfaces.core import AddressProvider, Platform, TokenInfo + + +class LetsBonkAddressProvider(AddressProvider): + """LetsBonk (Raydium LaunchLab) implementation of AddressProvider interface.""" + + # Raydium LaunchLab program addresses + RAYDIUM_LAUNCHLAB_PROGRAM_ID = Pubkey.from_string("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj") + GLOBAL_CONFIG = Pubkey.from_string("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX") + LETSBONK_PLATFORM_CONFIG = Pubkey.from_string("FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1") + + # System program addresses + TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") + SYSTEM_PROGRAM_ID = Pubkey.from_string("11111111111111111111111111111111") + WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112") + ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") + SYSTEM_RENT_PROGRAM_ID = Pubkey.from_string("SysvarRent111111111111111111111111111111111") + + @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 self.RAYDIUM_LAUNCHLAB_PROGRAM_ID + + def get_system_addresses(self) -> dict[str, Pubkey]: + """Get all system addresses required for LetsBonk. + + Returns: + Dictionary mapping address names to Pubkey objects + """ + return { + # Raydium LaunchLab specific addresses + "program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID, + "global_config": self.GLOBAL_CONFIG, + "platform_config": self.LETSBONK_PLATFORM_CONFIG, + + # System addresses + "system_program": self.SYSTEM_PROGRAM_ID, + "token_program": self.TOKEN_PROGRAM_ID, + "associated_token_program": self.ASSOCIATED_TOKEN_PROGRAM_ID, + "rent": self.SYSTEM_RENT_PROGRAM_ID, + "wsol_mint": self.WSOL_MINT, + } + + 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 = self.WSOL_MINT + + pool_state, _ = Pubkey.find_program_address( + [b"pool", bytes(base_mint), bytes(quote_mint)], + self.RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + return pool_state + + 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 if available + if token_info.pool_state: + accounts["pool_state"] = token_info.pool_state + + # Add vault addresses if available + if token_info.base_vault: + accounts["base_vault"] = token_info.base_vault + if token_info.quote_vault: + accounts["quote_vault"] = token_info.quote_vault + + # Derive pool state if not provided + if not token_info.pool_state: + accounts["pool_state"] = self.derive_pool_address(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], + self.RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + 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], + self.RAYDIUM_LAUNCHLAB_PROGRAM_ID + ) + 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, self.TOKEN_PROGRAM_ID) + + 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"], + "global_config": self.GLOBAL_CONFIG, + "platform_config": self.LETSBONK_PLATFORM_CONFIG, + "pool_state": additional_accounts["pool_state"], + "user_base_token": self.derive_user_token_account(user, token_info.mint), + "base_vault": additional_accounts.get("base_vault", token_info.base_vault), + "quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault), + "base_token_mint": token_info.mint, + "quote_token_mint": self.WSOL_MINT, + "base_token_program": self.TOKEN_PROGRAM_ID, + "quote_token_program": self.TOKEN_PROGRAM_ID, + "event_authority": additional_accounts["event_authority"], + "program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID, + } + + 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"], + "global_config": self.GLOBAL_CONFIG, + "platform_config": self.LETSBONK_PLATFORM_CONFIG, + "pool_state": additional_accounts["pool_state"], + "user_base_token": self.derive_user_token_account(user, token_info.mint), + "base_vault": additional_accounts.get("base_vault", token_info.base_vault), + "quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault), + "base_token_mint": token_info.mint, + "quote_token_mint": self.WSOL_MINT, + "base_token_program": self.TOKEN_PROGRAM_ID, + "quote_token_program": self.TOKEN_PROGRAM_ID, + "event_authority": additional_accounts["event_authority"], + "program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID, + } + + 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 + """ + return { + "payer": user, + "wsol_account": wsol_account, + "wsol_mint": self.WSOL_MINT, + "owner": user, + "system_program": self.SYSTEM_PROGRAM_ID, + "token_program": self.TOKEN_PROGRAM_ID, + "rent": self.SYSTEM_RENT_PROGRAM_ID, + } \ No newline at end of file diff --git a/src/platforms/letsbonk/curve_manager.py b/src/platforms/letsbonk/curve_manager.py new file mode 100644 index 0000000..243dae1 --- /dev/null +++ b/src/platforms/letsbonk/curve_manager.py @@ -0,0 +1,254 @@ +""" +LetsBonk implementation of CurveManager interface. + +This module handles LetsBonk (Raydium LaunchLab) specific pool operations +by implementing the CurveManager interface using IDL-based decoding. +""" + +import struct +from typing import Any + +from solders.pubkey import Pubkey + +from core.client import SolanaClient +from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS +from interfaces.core import CurveManager, Platform +from platforms.letsbonk.address_provider import LetsBonkAddressProvider +from utils.logger import get_logger + +logger = get_logger(__name__) + +# Pool state discriminator for Raydium LaunchLab +POOL_STATE_DISCRIMINATOR = bytes([247, 237, 227, 245, 215, 195, 222, 70]) + + +class LetsBonkCurveManager(CurveManager): + """LetsBonk (Raydium LaunchLab) implementation of CurveManager interface.""" + + def __init__(self, client: SolanaClient): + """Initialize LetsBonk curve manager. + + Args: + client: Solana RPC client + """ + self.client = client + self.address_provider = LetsBonkAddressProvider() + + @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 + """ + try: + 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 (simplified - in production you'd use IDL parser) + pool_state_data = self._decode_pool_state(account.data) + + return pool_state_data + + except Exception as e: + logger.error(f"Failed to get pool state: {e!s}") + 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 + ) -> 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 + ) -> 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(self, data: bytes) -> dict[str, Any]: + """Decode pool state data from raw bytes. + + This is a simplified decoder. In production, you should use the IDL parser. + + Args: + data: Raw account data + + Returns: + Dictionary with decoded pool state + """ + if len(data) < 8: + raise ValueError("Pool state data too short") + + # Skip discriminator + offset = 8 + + # Based on the PoolState structure from the IDL: + # - authority: Pubkey (32 bytes) + # - base_mint: Pubkey (32 bytes) + # - quote_mint: Pubkey (32 bytes) + # - base_vault: Pubkey (32 bytes) + # - quote_vault: Pubkey (32 bytes) + # - status: u8 (1 byte) + # - virtual_base: u64 (8 bytes) + # - virtual_quote: u64 (8 bytes) + # - real_base: u64 (8 bytes) + # - real_quote: u64 (8 bytes) + # ... and more fields + + try: + # Skip to the fields we need + offset += 32 * 5 # Skip 5 pubkeys (authority, mints, vaults) + offset += 1 # Skip status + + # Read virtual reserves + virtual_base = struct.unpack_from(" 0 else 0, + } + + except Exception as e: + logger.error(f"Failed to decode pool state: {e}") + # Return some default values for testing + return { + "virtual_base": 1_000_000_000, # 1000 tokens with 6 decimals + "virtual_quote": 1_000_000_000, # 1 SOL + "real_base": 1_000_000_000, + "real_quote": 1_000_000_000, + "price_per_token": 0.001, # 0.001 SOL per token + } + + async def get_pool_info(self, pool_address: Pubkey) -> dict[str, Any]: + """Get detailed pool information including status and progress. + + Args: + pool_address: Address of the pool state + + Returns: + Dictionary with pool information + """ + pool_state = await self.get_pool_state(pool_address) + + # Calculate additional metrics + sol_raised = pool_state["real_quote"] / LAMPORTS_PER_SOL + tokens_sold = (pool_state["virtual_base"] - pool_state["real_base"]) / 10**TOKEN_DECIMALS + + return { + "virtual_base_reserves": pool_state["virtual_base"], + "virtual_quote_reserves": pool_state["virtual_quote"], + "real_base_reserves": pool_state["real_base"], + "real_quote_reserves": pool_state["real_quote"], + "sol_raised": sol_raised, + "tokens_sold": tokens_sold, + "current_price": pool_state["price_per_token"], + } \ No newline at end of file diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py new file mode 100644 index 0000000..aa18242 --- /dev/null +++ b/src/platforms/letsbonk/event_parser.py @@ -0,0 +1,272 @@ +""" +LetsBonk implementation of EventParser interface. + +This module parses LetsBonk-specific token creation events from various sources +by implementing the EventParser interface. +""" + +import struct +from time import monotonic +from typing import Any, Final + +from solders.pubkey import Pubkey + +from interfaces.core import EventParser, Platform, TokenInfo +from platforms.letsbonk.address_provider import LetsBonkAddressProvider +from utils.logger import get_logger + +logger = get_logger(__name__) + + +class LetsBonkEventParser(EventParser): + """LetsBonk implementation of EventParser interface.""" + + # Discriminator for initialize instruction from IDL + INITIALIZE_DISCRIMINATOR: Final[bytes] = bytes([175, 175, 109, 31, 13, 152, 155, 237]) + INITIALIZE_DISCRIMINATOR_INT: Final[int] = 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 + ) -> 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] + ) -> TokenInfo | None: + """Parse token creation from LetsBonk 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 + """ + if not instruction_data.startswith(self.INITIALIZE_DISCRIMINATOR): + return None + + try: + # Helper to get account key + def get_account_key(index): + if index >= len(accounts): + return None + account_index = accounts[index] + if account_index >= len(account_keys): + return None + return Pubkey.from_bytes(account_keys[account_index]) + + # Parse instruction data + token_data = self._parse_initialize_instruction_data(instruction_data) + if not token_data: + return None + + # Extract account information based on IDL account order + creator = get_account_key(1) # creator account + pool_state = get_account_key(5) # pool_state account + base_mint = get_account_key(6) # base_mint account + base_vault = get_account_key(8) # base_vault account + quote_vault = get_account_key(9) # quote_vault account + + if not all([creator, pool_state, base_mint, base_vault, quote_vault]): + return None + + return TokenInfo( + name=token_data["name"], + symbol=token_data["symbol"], + uri=token_data["uri"], + mint=base_mint, + platform=Platform.LETS_BONK, + pool_state=pool_state, + base_vault=base_vault, + quote_vault=quote_vault, + user=creator, + creator=creator, + creation_timestamp=monotonic(), + ) + + except Exception: + return None + + def parse_token_creation_from_geyser( + 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'): + return None + + tx = transaction_info.transaction.transaction.transaction + msg = getattr(tx, "message", None) + if msg is None: + return None + + for ix in msg.instructions: + # Skip non-LetsBonk program instructions + program_idx = ix.program_id_index + if program_idx >= len(msg.account_keys): + continue + + program_id = msg.account_keys[program_idx] + if bytes(program_id) != bytes(self.get_program_id()): + continue + + # Check if it's the LetsBonk platform config account + has_platform_config = False + 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.LETSBONK_PLATFORM_CONFIG): + has_platform_config = True + break + + if not has_platform_config: + continue + + # Process instruction data + token_info = self.parse_token_creation_from_instruction( + ix.data, ix.accounts, msg.account_keys + ) + if token_info: + return token_info + + return None + + except Exception: + 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.RAYDIUM_LAUNCHLAB_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] + + def _parse_initialize_instruction_data(self, data: bytes) -> dict | None: + """Parse the initialize instruction data from LetsBonk. + + Args: + data: Raw instruction data + + Returns: + Dictionary of parsed data or None if parsing fails + """ + if len(data) < 8: + return None + + # Check discriminator + discriminator = struct.unpack(" len(data): + raise ValueError("Not enough data for string length") + length = struct.unpack_from(" len(data): + raise ValueError("Not enough data for string") + value = data[offset:offset + length].decode('utf-8') + offset += length + return value + + def read_u8(): + nonlocal offset + if offset + 1 > len(data): + raise ValueError("Not enough data for u8") + value = struct.unpack_from(" list[TokenInfo]: + """Parse token creations from block data (for block listener). + + Args: + block_data: Block data from WebSocket + + Returns: + List of TokenInfo for any token creations found + """ + tokens = [] + + try: + if "transactions" not in block_data: + return tokens + + for tx in block_data["transactions"]: + if not isinstance(tx, dict) or "transaction" not in tx: + continue + + # Process transaction (implementation would be similar to pump.fun) + # This is a simplified version - full implementation would decode + # the transaction and check for LetsBonk initialize instructions + pass + + return tokens + + except Exception: + return tokens \ No newline at end of file diff --git a/src/platforms/letsbonk/instruction_builder.py b/src/platforms/letsbonk/instruction_builder.py new file mode 100644 index 0000000..b4ff731 --- /dev/null +++ b/src/platforms/letsbonk/instruction_builder.py @@ -0,0 +1,402 @@ +""" +LetsBonk implementation of InstructionBuilder interface. + +This module builds LetsBonk (Raydium LaunchLab) specific buy and sell instructions +by implementing the InstructionBuilder interface. +""" + +import hashlib +import struct +import time +from typing import Final + +from solders.instruction import AccountMeta, Instruction +from solders.pubkey import Pubkey +from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed +from spl.token.instructions import create_idempotent_associated_token_account + +from core.pubkeys import TOKEN_DECIMALS +from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo + +# Instruction discriminators for LetsBonk (from IDL) +BUY_EXACT_IN_DISCRIMINATOR: Final[bytes] = bytes([250, 234, 13, 123, 213, 156, 19, 236]) +BUY_EXACT_OUT_DISCRIMINATOR: Final[bytes] = bytes([24, 211, 116, 40, 105, 3, 153, 56]) +SELL_EXACT_IN_DISCRIMINATOR: Final[bytes] = bytes([149, 39, 222, 155, 211, 124, 152, 26]) +SELL_EXACT_OUT_DISCRIMINATOR: Final[bytes] = bytes([95, 200, 71, 34, 8, 9, 11, 166]) + + +class LetsBonkInstructionBuilder(InstructionBuilder): + """LetsBonk (Raydium LaunchLab) implementation of InstructionBuilder interface.""" + + @property + def platform(self) -> 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 + ) -> 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 + token_info.mint, # mint + address_provider.TOKEN_PROGRAM_ID, # 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 = 2_039_280 # Standard account creation cost + total_lamports = amount_in + account_creation_lamports + + create_wsol_ix = create_account_with_seed( + CreateAccountWithSeedParams( + from_pubkey=user, + to_pubkey=wsol_account, + base=user, + seed=wsol_seed, + lamports=total_lamports, + space=165, # Size of a token account + owner=address_provider.TOKEN_PROGRAM_ID + ) + ) + instructions.append(create_wsol_ix) + + # 3. Initialize WSOL account + initialize_wsol_ix = self._create_initialize_account_instruction( + wsol_account, + address_provider.WSOL_MINT, + user, + address_provider + ) + instructions.append(initialize_wsol_ix) + + # 4. Build buy_exact_in instruction + buy_accounts = [ + AccountMeta(pubkey=accounts_info["payer"], is_signer=True, is_writable=True), + AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), + AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), + AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), + AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), + AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), + AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token + AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), + AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), + AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False), + AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False), + AccountMeta(pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False), + AccountMeta(pubkey=accounts_info["quote_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 + amount_in + minimum_amount_out + share_fee_rate + SHARE_FEE_RATE = 0 # No sharing fee + instruction_data = ( + 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 = 2_039_280 + + create_wsol_ix = create_account_with_seed( + CreateAccountWithSeedParams( + from_pubkey=user, + to_pubkey=wsol_account, + base=user, + seed=wsol_seed, + lamports=account_creation_lamports, + space=165, # Size of a token account + owner=address_provider.TOKEN_PROGRAM_ID + ) + ) + instructions.append(create_wsol_ix) + + # 2. Initialize WSOL account + initialize_wsol_ix = self._create_initialize_account_instruction( + wsol_account, + address_provider.WSOL_MINT, + user, + address_provider + ) + instructions.append(initialize_wsol_ix) + + # 3. Build sell_exact_in instruction + sell_accounts = [ + AccountMeta(pubkey=accounts_info["payer"], is_signer=True, is_writable=True), + AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), + AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), + AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), + AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), + AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), + AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token + AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), + AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), + AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False), + AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False), + AccountMeta(pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False), + AccountMeta(pubkey=accounts_info["quote_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 + amount_in + minimum_amount_out + share_fee_rate + SHARE_FEE_RATE = 0 # No sharing fee + instruction_data = ( + 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"], + accounts_info["base_vault"], + accounts_info["quote_vault"], + accounts_info["base_token_mint"], + accounts_info["quote_token_mint"], + accounts_info["program"], + ] + + def get_required_accounts_for_sell( + 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"], + accounts_info["base_vault"], + accounts_info["quote_vault"], + accounts_info["base_token_mint"], + accounts_info["quote_token_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, + address_provider: AddressProvider + ) -> Instruction: + """Create an InitializeAccount instruction for the Token Program. + + Args: + account: The account to initialize + mint: The token mint + owner: The account owner + address_provider: Platform address provider + + Returns: + Instruction for initializing the account + """ + accounts = [ + 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=address_provider.SYSTEM_RENT_PROGRAM_ID, is_signer=False, is_writable=False), + ] + + # InitializeAccount instruction discriminator (instruction 1 in Token Program) + data = bytes([1]) + + return Instruction( + program_id=address_provider.TOKEN_PROGRAM_ID, + data=data, + accounts=accounts + ) + + def _create_close_account_instruction( + self, + account: Pubkey, + destination: Pubkey, + owner: Pubkey, + address_provider: AddressProvider + ) -> 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) + address_provider: Platform address provider + + Returns: + Instruction for closing the account + """ + accounts = [ + AccountMeta(pubkey=account, is_signer=False, is_writable=True), + 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=address_provider.TOKEN_PROGRAM_ID, + 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 diff --git a/src/platforms/pumpfun/__init__.py b/src/platforms/pumpfun/__init__.py new file mode 100644 index 0000000..38c8f53 --- /dev/null +++ b/src/platforms/pumpfun/__init__.py @@ -0,0 +1,31 @@ +""" +Pump.Fun platform registration and exports. + +This module registers all pump.fun implementations with the platform factory +and provides convenient imports for the pump.fun platform. +""" + +from interfaces.core import Platform +from platforms import register_platform_implementations + +from .address_provider import PumpFunAddressProvider +from .curve_manager import PumpFunCurveManager +from .event_parser import PumpFunEventParser +from .instruction_builder import PumpFunInstructionBuilder + +# Register pump.fun platform implementations +register_platform_implementations( + Platform.PUMP_FUN, + PumpFunAddressProvider, + PumpFunInstructionBuilder, + PumpFunCurveManager, + PumpFunEventParser +) + +# Export implementations for direct use if needed +__all__ = [ + 'PumpFunAddressProvider', + 'PumpFunCurveManager', + 'PumpFunEventParser', + 'PumpFunInstructionBuilder' +] \ No newline at end of file diff --git a/src/platforms/pumpfun/address_provider.py b/src/platforms/pumpfun/address_provider.py new file mode 100644 index 0000000..cb64349 --- /dev/null +++ b/src/platforms/pumpfun/address_provider.py @@ -0,0 +1,216 @@ +""" +Pump.Fun implementation of AddressProvider interface. + +This module provides all pump.fun-specific addresses and PDA derivations +by implementing the AddressProvider interface. +""" + + +from solders.pubkey import Pubkey +from spl.token.instructions import get_associated_token_address + +from core.pubkeys import PumpAddresses, SystemAddresses +from interfaces.core import AddressProvider, Platform, TokenInfo + + +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 PumpAddresses.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 + """ + return { + # Pump.fun specific addresses + "program": PumpAddresses.PROGRAM, + "global": PumpAddresses.GLOBAL, + "event_authority": PumpAddresses.EVENT_AUTHORITY, + "fee": PumpAddresses.FEE, + "liquidity_migrator": PumpAddresses.LIQUIDITY_MIGRATOR, + + # System addresses + "system_program": SystemAddresses.PROGRAM, + "token_program": SystemAddresses.TOKEN_PROGRAM, + "associated_token_program": SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, + "rent": SystemAddresses.RENT, + "sol_mint": SystemAddresses.SOL, + } + + 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)], + PumpAddresses.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: + """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 + """ + return get_associated_token_address(bonding_curve, mint) + + 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)], + PumpAddresses.PROGRAM + ) + return creator_vault + + def derive_global_volume_accumulator(self) -> Pubkey: + """Derive the global volume accumulator PDA. + + Returns: + Global volume accumulator address + """ + return PumpAddresses.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 PumpAddresses.find_user_volume_accumulator(user) + + 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": PumpAddresses.GLOBAL, + "fee": PumpAddresses.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), + "user_token_account": self.derive_user_token_account(user, token_info.mint), + "user": user, + "system_program": SystemAddresses.PROGRAM, + "token_program": SystemAddresses.TOKEN_PROGRAM, + "creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault), + "event_authority": PumpAddresses.EVENT_AUTHORITY, + "program": PumpAddresses.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]: + """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": PumpAddresses.GLOBAL, + "fee": PumpAddresses.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), + "user_token_account": self.derive_user_token_account(user, token_info.mint), + "user": user, + "system_program": SystemAddresses.PROGRAM, + "creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault), + "token_program": SystemAddresses.TOKEN_PROGRAM, + "event_authority": PumpAddresses.EVENT_AUTHORITY, + "program": PumpAddresses.PROGRAM, + } \ No newline at end of file diff --git a/src/platforms/pumpfun/curve_manager.py b/src/platforms/pumpfun/curve_manager.py new file mode 100644 index 0000000..ef909c1 --- /dev/null +++ b/src/platforms/pumpfun/curve_manager.py @@ -0,0 +1,215 @@ +""" +Pump.Fun implementation of CurveManager interface. + +This module handles pump.fun-specific bonding curve operations +by implementing the CurveManager interface. +""" + +from typing import Any + +from solders.pubkey import Pubkey + +from core.client import SolanaClient +from core.curve import BondingCurveManager +from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS +from interfaces.core import CurveManager, Platform + + +class PumpFunCurveManager(CurveManager): + """Pump.Fun implementation of CurveManager interface.""" + + def __init__(self, client: SolanaClient): + """Initialize pump.fun curve manager. + + Args: + client: Solana RPC client + """ + self.client = client + self.bonding_curve_manager = BondingCurveManager(client) + + @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 + """ + curve_state = await self.bonding_curve_manager.get_curve_state(pool_address) + + return { + "virtual_token_reserves": curve_state.virtual_token_reserves, + "virtual_sol_reserves": curve_state.virtual_sol_reserves, + "real_token_reserves": curve_state.real_token_reserves, + "real_sol_reserves": curve_state.real_sol_reserves, + "token_total_supply": curve_state.token_total_supply, + "complete": curve_state.complete, + "creator": str(curve_state.creator), + + # Calculated fields for convenience + "price_per_token": curve_state.calculate_price(), + "token_reserves_decimal": curve_state.token_reserves, + "sol_reserves_decimal": curve_state.sol_reserves, + } + + 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 + """ + return await self.bonding_curve_manager.calculate_price(pool_address) + + async def calculate_buy_amount_out( + 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) + """ + curve_state = await self.bonding_curve_manager.get_curve_state(pool_address) + + # Use virtual reserves for bonding curve calculation + # Formula: tokens_out = (amount_in * virtual_token_reserves) / (virtual_sol_reserves + amount_in) + numerator = amount_in * curve_state.virtual_token_reserves + denominator = curve_state.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 + ) -> 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) + """ + curve_state = await self.bonding_curve_manager.get_curve_state(pool_address) + + # Use virtual reserves for bonding curve calculation + # Formula: sol_out = (amount_in * virtual_sol_reserves) / (virtual_token_reserves + amount_in) + numerator = amount_in * curve_state.virtual_sol_reserves + denominator = curve_state.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 + """ + curve_state = await self.bonding_curve_manager.get_curve_state(pool_address) + return (curve_state.virtual_token_reserves, curve_state.virtual_sol_reserves) + + 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 wraps calculate_buy_amount_out + and 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: + """Calculate the expected SOL amount for a given token input. + + This is a convenience method that wraps calculate_sell_amount_out + and 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 + """ + curve_state = await self.bonding_curve_manager.get_curve_state(pool_address) + return curve_state.complete + + 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 + """ + curve_state = await self.bonding_curve_manager.get_curve_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 = curve_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": curve_state.complete, + "sol_raised": sol_raised, + "estimated_target_sol": estimated_target_sol, + "progress_percentage": progress_percentage, + "tokens_available": curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS, + "market_cap_sol": sol_raised, # Approximate market cap + } \ No newline at end of file diff --git a/src/platforms/pumpfun/event_parser.py b/src/platforms/pumpfun/event_parser.py new file mode 100644 index 0000000..cccade0 --- /dev/null +++ b/src/platforms/pumpfun/event_parser.py @@ -0,0 +1,335 @@ +""" +Pump.Fun implementation of EventParser interface. + +This module parses pump.fun-specific token creation events from various sources +by implementing the EventParser interface. +""" + +import base64 +import struct +from time import monotonic +from typing import Any, Final + +import base58 +from solders.pubkey import Pubkey +from solders.transaction import VersionedTransaction + +from core.pubkeys import PumpAddresses, SystemAddresses +from interfaces.core import EventParser, Platform, TokenInfo + + +class PumpFunEventParser(EventParser): + """Pump.Fun implementation of EventParser interface.""" + + # Discriminators for pump.fun instructions + CREATE_DISCRIMINATOR: Final[int] = 8576854823835016728 + CREATE_DISCRIMINATOR_BYTES: Final[bytes] = struct.pack(" Platform: + """Get the platform this parser serves.""" + return Platform.PUMP_FUN + + def parse_token_creation_from_logs( + self, + logs: list[str], + signature: str + ) -> TokenInfo | None: + """Parse token creation from pump.fun transaction logs. + + Args: + logs: List of log strings from transaction + signature: Transaction signature + + Returns: + TokenInfo if token creation found, None otherwise + """ + # Check if this is a token creation + if not any("Program log: Instruction: Create" in log for log in logs): + return None + + # Skip swaps and other operations + if any("Program log: Instruction: CreateTokenAccount" in log for log in logs): + return None + + # Find and process program data + for log in logs: + if "Program data:" in log: + try: + encoded_data = log.split(": ")[1] + decoded_data = base64.b64decode(encoded_data) + return self._parse_create_instruction_data(decoded_data) + except Exception: + continue + + return None + + def parse_token_creation_from_instruction( + self, + instruction_data: bytes, + accounts: list[int], + account_keys: list[bytes] + ) -> TokenInfo | None: + """Parse token creation from pump.fun 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 + """ + if not instruction_data.startswith(self.CREATE_DISCRIMINATOR_BYTES): + return None + + try: + # Helper to get account key + def get_account_key(index): + if index >= len(accounts): + return None + account_index = accounts[index] + if account_index >= len(account_keys): + return None + return Pubkey.from_bytes(account_keys[account_index]) + + # Parse instruction data + token_data = self._parse_create_instruction_data(instruction_data) + if not token_data: + return None + + # Extract account information + mint = get_account_key(0) + bonding_curve = get_account_key(2) + associated_bonding_curve = get_account_key(3) + user = get_account_key(7) + + if not all([mint, bonding_curve, associated_bonding_curve, user]): + return None + + # Create creator vault + creator = Pubkey.from_string(token_data["creator"]) if token_data.get("creator") else user + creator_vault = self._derive_creator_vault(creator) + + return TokenInfo( + name=token_data["name"], + symbol=token_data["symbol"], + uri=token_data["uri"], + mint=mint, + platform=Platform.PUMP_FUN, + bonding_curve=bonding_curve, + associated_bonding_curve=associated_bonding_curve, + user=user, + creator=creator, + creator_vault=creator_vault, + creation_timestamp=monotonic(), + ) + + except Exception: + return None + + def parse_token_creation_from_geyser( + 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'): + return None + + tx = transaction_info.transaction.transaction.transaction + msg = getattr(tx, "message", None) + if msg is None: + return None + + for ix in msg.instructions: + # Skip non-pump.fun program instructions + program_idx = ix.program_id_index + if program_idx >= len(msg.account_keys): + continue + + program_id = msg.account_keys[program_idx] + if bytes(program_id) != bytes(self.get_program_id()): + continue + + # Process instruction data + token_info = self.parse_token_creation_from_instruction( + ix.data, ix.accounts, msg.account_keys + ) + if token_info: + return token_info + + return None + + except Exception: + return None + + def get_program_id(self) -> Pubkey: + """Get the pump.fun program ID this parser monitors. + + Returns: + Pump.fun program ID + """ + return PumpAddresses.PROGRAM + + def get_instruction_discriminators(self) -> list[bytes]: + """Get instruction discriminators for token creation. + + Returns: + List of discriminator bytes to match + """ + return [self.CREATE_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 + """ + try: + if "transactions" not in block_data: + return None + + for tx in block_data["transactions"]: + if not isinstance(tx, dict) or "transaction" not in tx: + continue + + # Decode base64 transaction data + tx_data_encoded = tx["transaction"][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] + + # 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(" dict | None: + """Parse the create instruction data from pump.fun. + + Args: + data: Raw instruction data + + Returns: + Dictionary of parsed data or None if parsing fails + """ + if len(data) < 8: + return None + + # Check for the correct instruction discriminator + discriminator = struct.unpack(" len(data): + return None + length = struct.unpack(" len(data): + return None + value = data[offset : offset + length].decode("utf-8") + offset += length + elif field_type == "publicKey": + if offset + 32 > len(data): + return None + value = base58.b58encode(data[offset : offset + 32]).decode("utf-8") + offset += 32 + + parsed_data[field_name] = value + + return parsed_data + + except Exception: + return None + + def _derive_creator_vault(self, creator: Pubkey) -> Pubkey: + """Derive the creator vault for a creator. + + Args: + creator: Creator address + + Returns: + Creator vault address + """ + derived_address, _ = Pubkey.find_program_address( + [b"creator-vault", bytes(creator)], + PumpAddresses.PROGRAM, + ) + return derived_address + + 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 + """ + derived_address, _ = Pubkey.find_program_address( + [ + bytes(bonding_curve), + bytes(SystemAddresses.TOKEN_PROGRAM), + bytes(mint), + ], + SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, + ) + return derived_address \ No newline at end of file diff --git a/src/platforms/pumpfun/instruction_builder.py b/src/platforms/pumpfun/instruction_builder.py new file mode 100644 index 0000000..e495828 --- /dev/null +++ b/src/platforms/pumpfun/instruction_builder.py @@ -0,0 +1,234 @@ +""" +Pump.Fun implementation of InstructionBuilder interface. + +This module builds pump.fun-specific buy and sell instructions +by implementing the InstructionBuilder interface. +""" + +import struct +from typing import Final + +from solders.instruction import AccountMeta, Instruction +from solders.pubkey import Pubkey +from spl.token.instructions import create_idempotent_associated_token_account + +from core.pubkeys import TOKEN_DECIMALS, SystemAddresses +from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo + +# Discriminators for pump.fun instructions +BUY_DISCRIMINATOR: Final[bytes] = struct.pack(" 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 + ) -> 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 + user, # owner + token_info.mint, # mint + 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["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["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), + ] + + # Build instruction data: discriminator + token_amount + max_sol_cost + instruction_data = ( + 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["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["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), + ] + + # Build instruction data: discriminator + token_amount + min_sol_output + instruction_data = ( + 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"], + accounts_info["associated_bonding_curve"], + accounts_info["user_token_account"], + accounts_info["fee"], + accounts_info["creator_vault"], + accounts_info["global_volume_accumulator"], + accounts_info["user_volume_accumulator"], + accounts_info["program"], + ] + + def get_required_accounts_for_sell( + 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"], + accounts_info["associated_bonding_curve"], + accounts_info["user_token_account"], + accounts_info["fee"], + 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 diff --git a/src/trading/base.py b/src/trading/base.py index 25a9862..ee7eb63 100644 --- a/src/trading/base.py +++ b/src/trading/base.py @@ -1,5 +1,9 @@ """ -Base interfaces for trading operations. +Enhanced base interfaces for trading operations with platform support. + +This module provides the complete enhanced base classes that replace the existing +trading/base.py while maintaining full backward compatibility. It integrates the +new interface system with the existing trading infrastructure. """ from abc import ABC, abstractmethod @@ -8,13 +12,16 @@ from typing import Any from solders.pubkey import Pubkey -from core.pubkeys import PumpAddresses +from interfaces.core import Platform + +# Import the new enhanced TokenInfo and Platform from interfaces +from interfaces.core import TokenInfo as EnhancedTokenInfo +# Keep the original TokenInfo structure for backward compatibility @dataclass -class TokenInfo: - """Token information.""" - +class TokenInfo_Legacy: + """Legacy token information structure for backward compatibility.""" name: str symbol: str uri: str @@ -26,14 +33,14 @@ class TokenInfo: creator_vault: Pubkey @classmethod - def from_dict(cls, data: dict[str, Any]) -> "TokenInfo": + def from_dict(cls, data: dict[str, Any]) -> "TokenInfo_Legacy": """Create TokenInfo from dictionary. Args: data: Dictionary with token data Returns: - TokenInfo instance + TokenInfo_Legacy instance """ return cls( name=data["name"], @@ -68,40 +75,353 @@ class TokenInfo: @dataclass class TradeResult: - """Result of a trading operation.""" - + """Enhanced result of a trading operation with platform support.""" success: bool + platform: Platform = Platform.PUMP_FUN # Add platform tracking tx_signature: str | None = None error_message: str | None = None amount: float | None = None price: float | None = None + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for logging/serialization. + + Returns: + Dictionary representation of the trade result + """ + return { + "success": self.success, + "platform": self.platform.value, + "tx_signature": self.tx_signature, + "error_message": self.error_message, + "amount": self.amount, + "price": self.price, + } + class Trader(ABC): - """Base interface for trading operations.""" + """Enhanced base interface for trading operations with platform support.""" @abstractmethod - async def execute(self, *args, **kwargs) -> TradeResult: + async def execute(self, token_info: "TokenInfo", *args, **kwargs) -> TradeResult: """Execute trading operation. + Args: + token_info: Enhanced token information with platform support + Returns: - TradeResult with operation outcome + TradeResult with operation outcome including platform info """ pass - def _get_relevant_accounts(self, token_info: TokenInfo) -> list[Pubkey]: + 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: - token_info: Token information for the buy/sell operation. + token_info: Enhanced token information Returns: - list[Pubkey]: List of relevant accounts. + List of relevant accounts (default implementation for pump.fun compatibility) """ - return [ - token_info.mint, # Token mint address - token_info.bonding_curve, # Bonding curve address - PumpAddresses.PROGRAM, # Pump.fun program address - PumpAddresses.FEE, # Pump.fun fee account - ] + # Default implementation maintains pump.fun compatibility + from core.pubkeys import PumpAddresses + + 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) + + # Add platform program + if token_info.platform == Platform.PUMP_FUN: + accounts.extend([ + PumpAddresses.PROGRAM, + PumpAddresses.FEE, + ]) + # Other platforms would add their specific accounts here + + return accounts + + +# Use the enhanced TokenInfo as the main TokenInfo class +# This provides the new functionality while maintaining the same import path +TokenInfo = EnhancedTokenInfo + + +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 + """ + return TokenInfo( + name=legacy_token_info.name, + symbol=legacy_token_info.symbol, + uri=legacy_token_info.uri, + mint=legacy_token_info.mint, + platform=Platform.PUMP_FUN, # Default to pump.fun for legacy tokens + bonding_curve=legacy_token_info.bonding_curve, + associated_bonding_curve=legacy_token_info.associated_bonding_curve, + user=legacy_token_info.user, + creator=legacy_token_info.creator, + creator_vault=legacy_token_info.creator_vault, + ) + + +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 + ]): + raise ValueError("Enhanced TokenInfo missing required pump.fun fields") + + return TokenInfo_Legacy( + name=enhanced_token_info.name, + symbol=enhanced_token_info.symbol, + uri=enhanced_token_info.uri, + mint=enhanced_token_info.mint, + bonding_curve=enhanced_token_info.bonding_curve, + associated_bonding_curve=enhanced_token_info.associated_bonding_curve, + user=enhanced_token_info.user or enhanced_token_info.creator, + creator=enhanced_token_info.creator or enhanced_token_info.user, + creator_vault=enhanced_token_info.creator_vault, + ) + + +def create_pump_fun_token_info( + name: str, + symbol: str, + uri: str, + mint: Pubkey, + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + user: Pubkey, + creator: Pubkey | None = None, + creator_vault: Pubkey | None = None, + **kwargs +) -> TokenInfo: + """Convenience function to create pump.fun TokenInfo with proper platform setting. + + Args: + name: Token name + symbol: Token symbol + uri: Token metadata URI + mint: Token mint address + bonding_curve: Bonding curve address + associated_bonding_curve: Associated bonding curve address + user: User/trader address + 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 + """ + from core.pubkeys import PumpAddresses + + # Default creator to user if not provided + if creator is None: + creator = user + + # Derive creator vault if not provided + if creator_vault is None: + creator_vault, _ = Pubkey.find_program_address( + [b"creator-vault", bytes(creator)], + PumpAddresses.PROGRAM, + ) + + return TokenInfo( + name=name, + symbol=symbol, + uri=uri, + mint=mint, + platform=Platform.PUMP_FUN, + bonding_curve=bonding_curve, + associated_bonding_curve=associated_bonding_curve, + user=user, + creator=creator, + creator_vault=creator_vault, + **kwargs + ) + + +def create_lets_bonk_token_info( + name: str, + symbol: str, + uri: str, + mint: Pubkey, + pool_state: Pubkey, + base_vault: Pubkey, + quote_vault: Pubkey, + user: Pubkey, + creator: Pubkey | None = None, + **kwargs +) -> TokenInfo: + """Convenience function to create LetsBonk TokenInfo with proper platform setting. + + Args: + name: Token name + symbol: Token symbol + uri: Token metadata URI + mint: Token mint address + pool_state: Pool state address + base_vault: Base token vault address + quote_vault: Quote token vault address + user: User/trader address + creator: Creator address (defaults to user if not provided) + **kwargs: Additional fields for TokenInfo + + Returns: + Enhanced TokenInfo configured for LetsBonk + """ + # Default creator to user if not provided + if creator is None: + creator = user + + return TokenInfo( + name=name, + symbol=symbol, + uri=uri, + mint=mint, + platform=Platform.LETS_BONK, + pool_state=pool_state, + base_vault=base_vault, + quote_vault=quote_vault, + user=user, + creator=creator, + **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 + """ + return token_info.platform == Platform.PUMP_FUN + + +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 + """ + return token_info.platform == Platform.LETS_BONK + + +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 + """ + if token_info.platform == Platform.PUMP_FUN: + return { + "bonding_curve": token_info.bonding_curve, + "associated_bonding_curve": token_info.associated_bonding_curve, + "creator_vault": token_info.creator_vault, + } + elif token_info.platform == Platform.LETS_BONK: + return { + "pool_state": token_info.pool_state, + "base_vault": token_info.base_vault, + "quote_vault": token_info.quote_vault, + } + else: + return {} + + +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, + ]): + 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, + ]) + elif token_info.platform == Platform.LETS_BONK: + return all([ + token_info.pool_state, + token_info.base_vault, + token_info.quote_vault, + ]) + + return True + + +# Backward compatibility exports +# This allows existing imports to continue working +__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', + # Convenience functions + 'create_pump_fun_token_info', + 'get_platform_specific_fields', + 'is_lets_bonk_token', + # Utility functions + 'is_pump_fun_token', + # Conversion functions + 'upgrade_token_info', + 'validate_token_info', +] \ No newline at end of file diff --git a/src/trading/platform_aware.py b/src/trading/platform_aware.py new file mode 100644 index 0000000..7770991 --- /dev/null +++ b/src/trading/platform_aware.py @@ -0,0 +1,315 @@ +""" +Platform-aware trader implementations that use the interface system. + +This module provides new trader classes that work with any platform +through the interface system, while maintaining compatibility with existing code. +""" + +from solders.pubkey import Pubkey + +from core.client import SolanaClient +from core.priority_fee.manager import PriorityFeeManager +from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS +from core.wallet import Wallet +from interfaces.core import AddressProvider, Platform, TokenInfo +from platforms import get_platform_implementations +from trading.base import Trader, TradeResult +from utils.logger import get_logger + +logger = get_logger(__name__) + + +class PlatformAwareBuyer(Trader): + """Platform-aware token buyer that works with any supported platform.""" + + def __init__( + self, + client: SolanaClient, + wallet: Wallet, + priority_fee_manager: PriorityFeeManager, + amount: float, + slippage: float = 0.01, + max_retries: int = 5, + extreme_fast_token_amount: int = 0, + extreme_fast_mode: bool = False, + ): + """Initialize platform-aware token buyer. + + Args: + client: Solana client for RPC calls + wallet: Wallet for signing transactions + priority_fee_manager: Priority fee manager + amount: Amount of SOL to spend + slippage: Slippage tolerance (0.01 = 1%) + max_retries: Maximum number of retry attempts + extreme_fast_token_amount: Amount of token to buy if extreme fast mode is enabled + extreme_fast_mode: If enabled, avoid fetching pool state for price estimation + """ + self.client = client + self.wallet = wallet + self.priority_fee_manager = priority_fee_manager + self.amount = amount + self.slippage = slippage + self.max_retries = max_retries + self.extreme_fast_mode = extreme_fast_mode + self.extreme_fast_token_amount = extreme_fast_token_amount + + async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult: + """Execute buy operation using platform-specific implementations. + + Args: + token_info: Enhanced token information with platform + + Returns: + TradeResult with buy outcome + """ + try: + # Get platform-specific implementations + implementations = get_platform_implementations(token_info.platform, self.client) + address_provider = implementations.address_provider + instruction_builder = implementations.instruction_builder + curve_manager = implementations.curve_manager + + # Convert amount to lamports + amount_lamports = int(self.amount * LAMPORTS_PER_SOL) + + if self.extreme_fast_mode: + # Skip the wait and directly calculate the amount + token_amount = self.extreme_fast_token_amount + token_price_sol = self.amount / token_amount if token_amount > 0 else 0 + else: + # Get pool address based on platform + 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 + + # Calculate minimum token amount with slippage + minimum_token_amount = token_amount * (1 - self.slippage) + minimum_token_amount_raw = int(minimum_token_amount * 10**TOKEN_DECIMALS) + + # Calculate maximum SOL to spend with slippage + max_amount_lamports = int(amount_lamports * (1 + self.slippage)) + + # Build buy instructions + instructions = await instruction_builder.build_buy_instruction( + token_info, + self.wallet.pubkey, + max_amount_lamports, # amount_in (SOL) + minimum_token_amount_raw, # minimum_amount_out (tokens) + address_provider + ) + + # Get accounts for priority fee calculation + priority_accounts = instruction_builder.get_required_accounts_for_buy( + token_info, self.wallet.pubkey, address_provider + ) + + logger.info( + f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token" + ) + logger.info( + f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)" + ) + + # Send transaction + tx_signature = await self.client.build_and_send_transaction( + instructions, + self.wallet.keypair, + skip_preflight=True, + max_retries=self.max_retries, + priority_fee=await self.priority_fee_manager.calculate_priority_fee( + priority_accounts + ), + ) + + success = await self.client.confirm_transaction(tx_signature) + + if success: + logger.info(f"Buy transaction confirmed: {tx_signature}") + return TradeResult( + success=True, + platform=token_info.platform, + tx_signature=tx_signature, + amount=token_amount, + price=token_price_sol, + ) + else: + return TradeResult( + success=False, + platform=token_info.platform, + error_message=f"Transaction failed to confirm: {tx_signature}", + ) + + except Exception as e: + logger.error(f"Buy operation failed: {e!s}") + return TradeResult( + success=False, + platform=token_info.platform, + error_message=str(e) + ) + + def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey: + """Get the pool/curve address for price calculations. + + Args: + token_info: Token information + address_provider: Platform address provider + + Returns: + Pool/curve address + """ + if token_info.platform == Platform.PUMP_FUN: + return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint) + elif token_info.platform == Platform.LETS_BONK: + return token_info.pool_state or address_provider.derive_pool_address(token_info.mint) + else: + # Fallback to deriving the address + return address_provider.derive_pool_address(token_info.mint) + + +class PlatformAwareSeller(Trader): + """Platform-aware token seller that works with any supported platform.""" + + def __init__( + self, + client: SolanaClient, + wallet: Wallet, + priority_fee_manager: PriorityFeeManager, + slippage: float = 0.25, + max_retries: int = 5, + ): + """Initialize platform-aware token seller. + + Args: + client: Solana client for RPC calls + wallet: Wallet for signing transactions + priority_fee_manager: Priority fee manager + slippage: Slippage tolerance (0.25 = 25%) + max_retries: Maximum number of retry attempts + """ + self.client = client + self.wallet = wallet + self.priority_fee_manager = priority_fee_manager + self.slippage = slippage + self.max_retries = max_retries + + async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult: + """Execute sell operation using platform-specific implementations. + + Args: + token_info: Enhanced token information with platform + + Returns: + TradeResult with sell outcome + """ + try: + # Get platform-specific implementations + implementations = get_platform_implementations(token_info.platform, self.client) + address_provider = implementations.address_provider + instruction_builder = implementations.instruction_builder + curve_manager = implementations.curve_manager + + # Get user's token account and balance + 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_decimal = token_balance / 10**TOKEN_DECIMALS + + logger.info(f"Token balance: {token_balance_decimal}") + + if token_balance == 0: + logger.info("No tokens to sell.") + return TradeResult( + success=False, + platform=token_info.platform, + error_message="No tokens to sell" + ) + + # Get pool address and current price + pool_address = self._get_pool_address(token_info, address_provider) + token_price_sol = await curve_manager.calculate_price(pool_address) + + logger.info(f"Price per Token: {token_price_sol:.8f} SOL") + + # 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) + + logger.info(f"Selling {token_balance_decimal} tokens") + logger.info(f"Expected SOL output: {expected_sol_output:.8f} SOL") + logger.info( + f"Minimum SOL output (with {self.slippage * 100}% slippage): {min_sol_output / LAMPORTS_PER_SOL:.8f} SOL" + ) + + # Build sell instructions + instructions = await instruction_builder.build_sell_instruction( + token_info, + self.wallet.pubkey, + token_balance, # amount_in (tokens) + min_sol_output, # minimum_amount_out (SOL) + address_provider + ) + + # Get accounts for priority fee calculation + priority_accounts = instruction_builder.get_required_accounts_for_sell( + token_info, self.wallet.pubkey, address_provider + ) + + # Send transaction + tx_signature = await self.client.build_and_send_transaction( + instructions, + self.wallet.keypair, + skip_preflight=True, + max_retries=self.max_retries, + priority_fee=await self.priority_fee_manager.calculate_priority_fee( + priority_accounts + ), + ) + + success = await self.client.confirm_transaction(tx_signature) + + if success: + logger.info(f"Sell transaction confirmed: {tx_signature}") + return TradeResult( + success=True, + platform=token_info.platform, + tx_signature=tx_signature, + amount=token_balance_decimal, + price=token_price_sol, + ) + else: + return TradeResult( + success=False, + platform=token_info.platform, + error_message=f"Transaction failed to confirm: {tx_signature}", + ) + + except Exception as e: + logger.error(f"Sell operation failed: {e!s}") + return TradeResult( + success=False, + platform=token_info.platform, + error_message=str(e) + ) + + def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey: + """Get the pool/curve address for price calculations. + + Args: + token_info: Token information + address_provider: Platform address provider + + Returns: + Pool/curve address + """ + if token_info.platform == Platform.PUMP_FUN: + return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint) + elif token_info.platform == Platform.LETS_BONK: + return token_info.pool_state or address_provider.derive_pool_address(token_info.mint) + else: + # Fallback to deriving the address + return address_provider.derive_pool_address(token_info.mint) \ No newline at end of file From 9ec9eeb793b141d77a1356956e96653ab16dc652 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 11:30:30 +0000 Subject: [PATCH 02/30] wip(core): platform aware trading --- src/bot_runner.py | 60 ++- src/config_loader.py | 2 +- src/core/curve.py | 140 ------ src/interfaces/__init__.py | 280 ------------ src/monitoring/base_listener.py | 27 +- src/monitoring/block_event_processor.py | 190 -------- src/monitoring/geyser_event_processor.py | 125 ------ src/monitoring/listener_factory.py | 137 ++++++ src/monitoring/logs_event_processor.py | 174 -------- ...istener.py => universal_block_listener.py} | 134 ++++-- ...stener.py => universal_geyser_listener.py} | 99 +++-- ...listener.py => universal_logs_listener.py} | 138 ++++-- src/platforms/__init__.py | 56 ++- src/platforms/letsbonk/__init__.py | 18 +- src/platforms/pumpfun/__init__.py | 18 +- src/trading/base.py | 182 ++++---- src/trading/buyer.py | 416 ------------------ src/trading/seller.py | 213 --------- .../{trader.py => universal_trader.py} | 355 ++++++--------- 19 files changed, 726 insertions(+), 2038 deletions(-) delete mode 100644 src/core/curve.py delete mode 100644 src/interfaces/__init__.py delete mode 100644 src/monitoring/block_event_processor.py delete mode 100644 src/monitoring/geyser_event_processor.py create mode 100644 src/monitoring/listener_factory.py delete mode 100644 src/monitoring/logs_event_processor.py rename src/monitoring/{block_listener.py => universal_block_listener.py} (53%) rename src/monitoring/{geyser_listener.py => universal_geyser_listener.py} (60%) rename src/monitoring/{logs_listener.py => universal_logs_listener.py} (51%) delete mode 100644 src/trading/buyer.py delete mode 100644 src/trading/seller.py rename src/trading/{trader.py => universal_trader.py} (65%) diff --git a/src/bot_runner.py b/src/bot_runner.py index 9a73351..0dc79da 100644 --- a/src/bot_runner.py +++ b/src/bot_runner.py @@ -8,8 +8,12 @@ import uvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) -from config_loader import load_bot_config, print_config_summary -from trading.trader import PumpTrader +from config_loader import ( + get_platform_from_config, + load_bot_config, + print_config_summary, +) +from trading.universal_trader import UniversalTrader from utils.logger import setup_file_logging @@ -40,34 +44,48 @@ async def start_bot(config_path: str): setup_logging(cfg["name"]) print_config_summary(cfg) - trader = PumpTrader( + # Get platform from configuration + platform = get_platform_from_config(cfg) + + # Initialize universal trader with platform-specific configuration + trader = UniversalTrader( # Connection settings rpc_endpoint=cfg["rpc_endpoint"], wss_endpoint=cfg["wss_endpoint"], private_key=cfg["private_key"], + + # Platform configuration + platform=platform, + # Trade parameters buy_amount=cfg["trade"]["buy_amount"], buy_slippage=cfg["trade"]["buy_slippage"], sell_slippage=cfg["trade"]["sell_slippage"], + # Extreme fast mode settings extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False), extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30), + # Exit strategy configuration exit_strategy=cfg["trade"].get("exit_strategy", "time_based"), take_profit_percentage=cfg["trade"].get("take_profit_percentage"), stop_loss_percentage=cfg["trade"].get("stop_loss_percentage"), max_hold_time=cfg["trade"].get("max_hold_time"), price_check_interval=cfg["trade"].get("price_check_interval", 10), + # Listener configuration listener_type=cfg["filters"]["listener_type"], + # Geyser configuration (if applicable) geyser_endpoint=cfg.get("geyser", {}).get("endpoint"), geyser_api_token=cfg.get("geyser", {}).get("api_token"), - geyser_auth_type=cfg.get("geyser", {}).get("auth_type"), + geyser_auth_type=cfg.get("geyser", {}).get("auth_type", "x-token"), + # PumpPortal configuration (if applicable) pumpportal_url=cfg.get("pumpportal", {}).get( "url", "wss://pumpportal.fun/api/data" ), + # Priority fee configuration enable_dynamic_priority_fee=cfg.get("priority_fees", {}).get( "enable_dynamic", False @@ -78,6 +96,7 @@ async def start_bot(config_path: str): fixed_priority_fee=cfg.get("priority_fees", {}).get("fixed_amount", 500000), extra_priority_fee=cfg.get("priority_fees", {}).get("extra_percentage", 0.0), hard_cap_prior_fee=cfg.get("priority_fees", {}).get("hard_cap", 500000), + # Retry and timeout settings max_retries=cfg.get("retries", {}).get("max_attempts", 10), wait_time_after_creation=cfg.get("retries", {}).get("wait_after_creation", 15), @@ -85,8 +104,9 @@ async def start_bot(config_path: str): wait_time_before_new_token=cfg.get("retries", {}).get( "wait_before_new_token", 15 ), - max_token_age=cfg.get("timing", {}).get("max_token_age", 0.001), + max_token_age=cfg.get("filters", {}).get("max_token_age", 0.001), token_wait_timeout=cfg.get("timing", {}).get("token_wait_timeout", 120), + # Cleanup settings cleanup_mode=cfg.get("cleanup", {}).get("mode", "disabled"), cleanup_force_close_with_burn=cfg.get("cleanup", {}).get( @@ -95,6 +115,7 @@ async def start_bot(config_path: str): cleanup_with_priority_fee=cfg.get("cleanup", {}).get( "with_priority_fee", False ), + # Trading filters match_string=cfg["filters"].get("match_string"), bro_address=cfg["filters"].get("bro_address"), @@ -141,21 +162,34 @@ def run_all_bots(): skipped_bots += 1 continue + # Validate platform support + try: + platform = get_platform_from_config(cfg) + from platforms import platform_factory + if not platform_factory.registry.is_platform_supported(platform): + logging.error(f"Platform {platform.value} is not supported for bot '{bot_name}'. Skipping...") + skipped_bots += 1 + continue + except Exception as e: + logging.exception(f"Invalid platform configuration for bot '{bot_name}': {e}. Skipping...") + skipped_bots += 1 + continue + if cfg.get("separate_process", False): - logging.info(f"Starting bot '{bot_name}' in separate process") + logging.info(f"Starting bot '{bot_name}' ({platform.value}) in separate process") p = multiprocessing.Process( target=run_bot_process, args=(str(file),), name=f"bot-{bot_name}" ) p.start() processes.append(p) else: - logging.info(f"Starting bot '{bot_name}' in main process") + logging.info(f"Starting bot '{bot_name}' ({platform.value}) in main process") asyncio.run(start_bot(str(file))) except Exception as e: logging.exception(f"Failed to start bot from {file}: {e}") logging.info( - f"Started {len(bot_files) - skipped_bots} bots, skipped {skipped_bots} disabled bots" + f"Started {len(bot_files) - skipped_bots} bots, skipped {skipped_bots} disabled/invalid bots" ) for p in processes: @@ -169,8 +203,16 @@ def main() -> None: format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) + # Log supported platforms + 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]}") + except Exception as e: + logging.warning(f"Could not load platform information: {e}") + run_all_bots() if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/src/config_loader.py b/src/config_loader.py index 79e3c47..8d0313d 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -79,7 +79,7 @@ VALID_VALUES = { "filters.listener_type": ["logs", "blocks", "geyser", "pumpportal"], "cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"], "trade.exit_strategy": ["time_based", "tp_sl", "manual"], - "platform": ["pump_fun", "lets_bonk"], # New platform validation + "platform": ["pump_fun", "lets_bonk"], # Platform validation } diff --git a/src/core/curve.py b/src/core/curve.py deleted file mode 100644 index e56651d..0000000 --- a/src/core/curve.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -Bonding curve operations for pump.fun tokens. -""" - -import struct -from typing import Final - -from construct import Bytes, Flag, Int64ul, Struct -from solders.pubkey import Pubkey - -from core.client import SolanaClient -from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS -from utils.logger import get_logger - -logger = get_logger(__name__) - -# Discriminator for the bonding curve account -EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(" None: - """Parse bonding curve data. - - Args: - data: Raw account data - - Raises: - ValueError: If data cannot be parsed - """ - if data[:8] != EXPECTED_DISCRIMINATOR: - raise ValueError("Invalid curve state discriminator") - - parsed = self._STRUCT.parse(data[8:]) - self.__dict__.update(parsed) - - # Convert raw bytes to Pubkey for creator field - if hasattr(self, "creator") and isinstance(self.creator, bytes): - self.creator = Pubkey.from_bytes(self.creator) - - def calculate_price(self) -> float: - """Calculate token price in SOL. - - Returns: - Token price in SOL - - Raises: - ValueError: If reserve state is invalid - """ - if self.virtual_token_reserves <= 0 or self.virtual_sol_reserves <= 0: - raise ValueError("Invalid reserve state") - - return (self.virtual_sol_reserves / LAMPORTS_PER_SOL) / ( - self.virtual_token_reserves / 10**TOKEN_DECIMALS - ) - - @property - def token_reserves(self) -> float: - """Get token reserves in decimal form.""" - return self.virtual_token_reserves / 10**TOKEN_DECIMALS - - @property - def sol_reserves(self) -> float: - """Get SOL reserves in decimal form.""" - return self.virtual_sol_reserves / LAMPORTS_PER_SOL - - -class BondingCurveManager: - """Manager for bonding curve operations.""" - - def __init__(self, client: SolanaClient): - """Initialize with Solana client. - - Args: - client: Solana client for RPC calls - """ - self.client = client - - async def get_curve_state(self, curve_address: Pubkey) -> BondingCurveState: - """Get the state of a bonding curve. - - Args: - curve_address: Address of the bonding curve account - - Returns: - Bonding curve state - - Raises: - ValueError: If curve data is invalid - """ - try: - account = await self.client.get_account_info(curve_address) - if not account.data: - raise ValueError(f"No data in bonding curve account {curve_address}") - - return BondingCurveState(account.data) - - except Exception as e: - logger.error(f"Failed to get curve state: {e!s}") - raise ValueError(f"Invalid curve state: {e!s}") - - async def calculate_price(self, curve_address: Pubkey) -> float: - """Calculate the current price of a token. - - Args: - curve_address: Address of the bonding curve account - - Returns: - Token price in SOL - """ - curve_state = await self.get_curve_state(curve_address) - return curve_state.calculate_price() - - async def calculate_expected_tokens( - self, curve_address: Pubkey, sol_amount: float - ) -> float: - """Calculate the expected token amount for a given SOL input. - - Args: - curve_address: Address of the bonding curve account - sol_amount: Amount of SOL to spend - - Returns: - Expected token amount - """ - curve_state = await self.get_curve_state(curve_address) - price = curve_state.calculate_price() - return sol_amount / price diff --git a/src/interfaces/__init__.py b/src/interfaces/__init__.py deleted file mode 100644 index c2b3dbf..0000000 --- a/src/interfaces/__init__.py +++ /dev/null @@ -1,280 +0,0 @@ -""" -Platform factory and registry for managing multiple trading platforms. - -This module provides a centralized way to instantiate and access -platform-specific implementations of the trading interfaces. -""" - -from dataclasses import dataclass -from typing import Any, Dict, Optional, Type - -from core.client import SolanaClient -from interfaces.core import ( - AddressProvider, - CurveManager, - EventParser, - InstructionBuilder, - Platform, -) - - -@dataclass -class PlatformImplementations: - """Container for all platform-specific implementations.""" - address_provider: AddressProvider - instruction_builder: InstructionBuilder - curve_manager: CurveManager - event_parser: EventParser - - -class PlatformRegistry: - """Registry for platform implementations.""" - - def __init__(self): - self._implementations: dict[Platform, dict[str, type]] = {} - self._instances: dict[Platform, 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] - ) -> None: - """Register platform implementations. - - Args: - platform: Platform enum value - address_provider_class: AddressProvider implementation class - instruction_builder_class: InstructionBuilder implementation class - curve_manager_class: CurveManager implementation class - 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 - } - - def create_platform_implementations( - self, - platform: Platform, - client: SolanaClient, - **kwargs: Any - ) -> PlatformImplementations: - """Create platform implementation instances. - - 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") - - # Check if we already have instances for this platform - if platform in self._instances: - return self._instances[platform] - - impl_classes = self._implementations[platform] - - # Create instances - address_provider = impl_classes['address_provider']() - 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 - ) - - # Cache the instances - self._instances[platform] = implementations - - return implementations - - def get_platform_implementations(self, platform: Platform) -> PlatformImplementations | None: - """Get cached platform implementations. - - Args: - platform: Platform to get implementations for - - Returns: - PlatformImplementations if available, None otherwise - """ - return self._instances.get(platform) - - 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 - - -class PlatformFactory: - """Factory for creating platform-specific implementations.""" - - def __init__(self): - self.registry = PlatformRegistry() - self._setup_default_platforms() - - def _setup_default_platforms(self) -> None: - """Setup default platform registrations. - - This will be populated as we implement each platform. - """ - # Platforms will be registered here as they are implemented - # Example: - # from platforms.pumpfun import PumpFunAddressProvider, PumpFunInstructionBuilder, ... - # self.registry.register_platform( - # Platform.PUMP_FUN, - # PumpFunAddressProvider, - # PumpFunInstructionBuilder, - # PumpFunCurveManager, - # PumpFunEventParser - # ) - pass - - def create_for_platform( - 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 - - 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: - """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) - return implementations.address_provider - - 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) - return implementations.instruction_builder - - 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) - 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) - 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() - - -# Global factory instance -platform_factory = PlatformFactory() - - -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 - """ - return platform_factory.create_for_platform(platform, client) - - -def register_platform_implementations( - platform: Platform, - address_provider_class: type[AddressProvider], - instruction_builder_class: type[InstructionBuilder], - curve_manager_class: type[CurveManager], - event_parser_class: type[EventParser] -) -> None: - """Register platform implementations with the global factory. - - Args: - platform: Platform enum value - address_provider_class: AddressProvider implementation class - instruction_builder_class: InstructionBuilder implementation class - curve_manager_class: CurveManager implementation class - event_parser_class: EventParser implementation class - """ - platform_factory.registry.register_platform( - platform, - address_provider_class, - instruction_builder_class, - curve_manager_class, - event_parser_class - ) \ No newline at end of file diff --git a/src/monitoring/base_listener.py b/src/monitoring/base_listener.py index 0a5e191..10184e2 100644 --- a/src/monitoring/base_listener.py +++ b/src/monitoring/base_listener.py @@ -1,15 +1,23 @@ """ -Base class for WebSocket token listeners. +Base class for WebSocket token listeners - now platform-agnostic. """ from abc import ABC, abstractmethod from collections.abc import Awaitable, Callable -from trading.base import TokenInfo +from interfaces.core import Platform, TokenInfo class BaseTokenListener(ABC): - """Base abstract class for token listeners.""" + """Base abstract class for token listeners - now platform-agnostic.""" + + def __init__(self, platform: Platform | None = None): + """Initialize the listener with optional platform specification. + + Args: + platform: Platform to monitor (if None, monitor all platforms) + """ + self.platform = platform @abstractmethod async def listen_for_tokens( @@ -27,3 +35,16 @@ class BaseTokenListener(ABC): creator_address: Optional creator address to filter by """ pass + + 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 diff --git a/src/monitoring/block_event_processor.py b/src/monitoring/block_event_processor.py deleted file mode 100644 index a3defbe..0000000 --- a/src/monitoring/block_event_processor.py +++ /dev/null @@ -1,190 +0,0 @@ -""" -Event processing for pump.fun tokens. -""" - -import base64 -import json -import struct -from typing import Any - -import base58 -from solders.pubkey import Pubkey -from solders.transaction import VersionedTransaction - -from core.pubkeys import PumpAddresses -from trading.base import TokenInfo -from utils.logger import get_logger - -logger = get_logger(__name__) - - -class PumpEventProcessor: - """Processes events from pump.fun program.""" - - # Discriminator for create instruction - CREATE_DISCRIMINATOR = 8576854823835016728 - - def __init__(self, pump_program: Pubkey): - """Initialize event processor. - - Args: - pump_program: Pump.fun program address - """ - self.pump_program = pump_program - self._idl = self._load_idl() - - def _load_idl(self) -> dict[str, Any]: - """Load IDL from file. - - Returns: - IDL as dictionary - """ - try: - with open("idl/pump_fun_idl.json") as f: - return json.load(f) - except Exception as e: - logger.error(f"Failed to load IDL: {e!s}") - # Create a minimal IDL with just what we need - return { - "instructions": [ - { - "name": "create", - "args": [ - {"name": "name", "type": "string"}, - {"name": "symbol", "type": "string"}, - {"name": "uri", "type": "string"}, - ], - } - ] - } - - def process_transaction(self, tx_data: str) -> TokenInfo | None: - """Process a transaction and extract token info. - - Args: - tx_data: Base64 encoded transaction data - - Returns: - TokenInfo if a token creation is found, None otherwise - """ - try: - tx_data_decoded = base64.b64decode(tx_data) - transaction = VersionedTransaction.from_bytes(tx_data_decoded) - - for ix in transaction.message.instructions: - # Check if instruction is from pump.fun program - program_id_index = ix.program_id_index - if program_id_index >= len(transaction.message.account_keys): - continue - - program_id = transaction.message.account_keys[program_id_index] - - if str(program_id) != str(self.pump_program): - continue - - ix_data = bytes(ix.data) - - # Check if it's a create instruction - if len(ix_data) < 8: - continue - - discriminator = struct.unpack(" dict[str, Any]: - """Decode create instruction data. - - Args: - ix_data: Instruction data bytes - ix_def: Instruction definition from IDL - accounts: List of account pubkeys - - Returns: - Decoded instruction arguments - """ - args = {} - offset = 8 # Skip 8-byte discriminator - - for arg in ix_def["args"]: - if arg["type"] == "string": - length = struct.unpack_from(" Pubkey: - """ - Find the creator vault for a creator. - - Args: - creator: Creator address - - Returns: - Creator vault address - """ - derived_address, _ = Pubkey.find_program_address( - [b"creator-vault", bytes(creator)], - PumpAddresses.PROGRAM, - ) - return derived_address diff --git a/src/monitoring/geyser_event_processor.py b/src/monitoring/geyser_event_processor.py deleted file mode 100644 index 3331c1b..0000000 --- a/src/monitoring/geyser_event_processor.py +++ /dev/null @@ -1,125 +0,0 @@ -""" -Event processing for pump.fun tokens using Geyser data. -""" - -import struct -from typing import Final - -import base58 -from solders.pubkey import Pubkey - -from core.pubkeys import PumpAddresses -from trading.base import TokenInfo -from utils.logger import get_logger - -logger = get_logger(__name__) - - -class GeyserEventProcessor: - """Processes token creation events from Geyser stream.""" - - CREATE_DISCRIMINATOR: Final[bytes] = struct.pack(" TokenInfo | None: - """Process transaction data and extract token creation info. - - Args: - instruction_data: Raw instruction data - accounts: List of account indices - keys: List of account public keys - - Returns: - TokenInfo if token creation found, None otherwise - """ - if not instruction_data.startswith(self.CREATE_DISCRIMINATOR): - return None - - try: - # Skip past the 8-byte discriminator - offset = 8 - - # Helper to read strings (prefixed with length) - def read_string(): - nonlocal offset - # Get string length (4-byte uint) - length = struct.unpack_from("= len(accounts): - return None - account_index = accounts[index] - if account_index >= len(keys): - return None - return Pubkey.from_bytes(keys[account_index]) - - name = read_string() - symbol = read_string() - uri = read_string() - creator = read_pubkey() - - mint = get_account_key(0) - bonding_curve = get_account_key(2) - associated_bonding_curve = get_account_key(3) - user = get_account_key(7) - - creator_vault = self._find_creator_vault(creator) - - if not all([mint, bonding_curve, associated_bonding_curve, user]): - logger.warning("Missing required account keys in token creation") - return None - - return TokenInfo( - name=name, - symbol=symbol, - uri=uri, - mint=mint, - bonding_curve=bonding_curve, - associated_bonding_curve=associated_bonding_curve, - user=user, - creator=creator, - creator_vault=creator_vault, - ) - - except Exception as e: - logger.error(f"Failed to process transaction data: {e}") - return None - - def _find_creator_vault(self, creator: Pubkey) -> Pubkey: - """ - Find the creator vault for a creator. - - Args: - creator: Creator address - - Returns: - Creator vault address - """ - derived_address, _ = Pubkey.find_program_address( - [b"creator-vault", bytes(creator)], - PumpAddresses.PROGRAM, - ) - return derived_address diff --git a/src/monitoring/listener_factory.py b/src/monitoring/listener_factory.py new file mode 100644 index 0000000..93dca13 --- /dev/null +++ b/src/monitoring/listener_factory.py @@ -0,0 +1,137 @@ +""" +Factory for creating platform-aware token listeners. +""" + +from interfaces.core import Platform +from monitoring.base_listener import BaseTokenListener +from utils.logger import get_logger + +logger = get_logger(__name__) + + +class ListenerFactory: + """Factory for creating appropriate token listeners based on configuration.""" + + @staticmethod + def create_listener( + listener_type: str, + wss_endpoint: str | None = None, + geyser_endpoint: str | None = None, + geyser_api_token: str | None = None, + geyser_auth_type: str = "x-token", + pumpportal_url: str = "wss://pumpportal.fun/api/data", + platforms: list[Platform] | None = None, + **kwargs + ) -> BaseTokenListener: + """Create a token listener based on the specified type. + + Args: + listener_type: Type of listener ('logs', 'blocks', 'geyser', or 'pumpportal') + wss_endpoint: WebSocket endpoint URL (for logs/blocks listeners) + geyser_endpoint: Geyser gRPC endpoint URL (for geyser listener) + geyser_api_token: Geyser API token (for geyser listener) + geyser_auth_type: Geyser authentication type + pumpportal_url: PumpPortal WebSocket URL (for pumpportal listener) + platforms: List of platforms to monitor (if None, monitor all) + **kwargs: Additional arguments + + Returns: + Configured token listener + + Raises: + ValueError: If listener type is invalid or required parameters are missing + """ + listener_type = listener_type.lower() + + if listener_type == "geyser": + if not geyser_endpoint or not geyser_api_token: + raise ValueError( + "Geyser endpoint and API token are required for geyser listener" + ) + + from monitoring.universal_geyser_listener import UniversalGeyserListener + + listener = UniversalGeyserListener( + geyser_endpoint=geyser_endpoint, + geyser_api_token=geyser_api_token, + geyser_auth_type=geyser_auth_type, + platforms=platforms, + ) + logger.info("Created Universal Geyser listener for token monitoring") + return listener + + elif listener_type == "logs": + if not wss_endpoint: + raise ValueError("WebSocket endpoint is required for logs listener") + + from monitoring.universal_logs_listener import UniversalLogsListener + + listener = UniversalLogsListener( + wss_endpoint=wss_endpoint, + platforms=platforms, + ) + logger.info("Created Universal Logs listener for token monitoring") + return listener + + elif listener_type == "blocks": + if not wss_endpoint: + raise ValueError("WebSocket endpoint is required for blocks listener") + + from monitoring.universal_block_listener import UniversalBlockListener + + listener = UniversalBlockListener( + wss_endpoint=wss_endpoint, + platforms=platforms, + ) + logger.info("Created Universal Block listener for token monitoring") + return listener + + elif listener_type == "pumpportal": + # PumpPortal is pump.fun specific, so filter platforms + pumpfun_platforms = [Platform.PUMP_FUN] + if platforms: + pumpfun_platforms = [p for p in platforms if p == Platform.PUMP_FUN] + + if not pumpfun_platforms: + raise ValueError("PumpPortal listener only supports pump.fun platform") + + from monitoring.pumpportal_listener import PumpPortalListener + + listener = PumpPortalListener( + pump_program=None, # Will be determined from platform + pumpportal_url=pumpportal_url, + ) + logger.info("Created PumpPortal listener for token monitoring") + return listener + + else: + raise ValueError( + f"Invalid listener type '{listener_type}'. " + f"Must be one of: 'logs', 'blocks', 'geyser', 'pumpportal'" + ) + + @staticmethod + def get_supported_listener_types() -> list[str]: + """Get list of supported listener types. + + Returns: + List of supported listener type strings + """ + return ["logs", "blocks", "geyser", "pumpportal"] + + @staticmethod + def get_platform_compatible_listeners(platform: Platform) -> list[str]: + """Get list of listener types compatible with a specific platform. + + Args: + platform: Platform to check compatibility for + + Returns: + List of compatible listener types + """ + if platform == Platform.PUMP_FUN: + return ["logs", "blocks", "geyser", "pumpportal"] + elif platform == Platform.LETS_BONK: + return ["logs", "blocks", "geyser"] # PumpPortal is pump.fun only + else: + return ["logs", "blocks", "geyser"] # Default universal listeners \ No newline at end of file diff --git a/src/monitoring/logs_event_processor.py b/src/monitoring/logs_event_processor.py deleted file mode 100644 index b1bca10..0000000 --- a/src/monitoring/logs_event_processor.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -Event processing for pump.fun tokens using logsSubscribe data. -""" - -import base64 -import struct -from typing import Final - -import base58 -from solders.pubkey import Pubkey - -from core.pubkeys import PumpAddresses, SystemAddresses -from trading.base import TokenInfo -from utils.logger import get_logger - -logger = get_logger(__name__) - - -class LogsEventProcessor: - """Processes events from pump.fun program logs.""" - - # Discriminator for create instruction to avoid non-create transactions - CREATE_DISCRIMINATOR: Final[int] = 8530921459188068891 - - def __init__(self, pump_program: Pubkey): - """Initialize event processor. - - Args: - pump_program: Pump.fun program address - """ - self.pump_program = pump_program - - def process_program_logs(self, logs: list[str], signature: str) -> TokenInfo | None: - """Process program logs and extract token info. - - Args: - logs: List of log strings from the notification - signature: Transaction signature - - Returns: - TokenInfo if a token creation is found, None otherwise - """ - # Check if this is a token creation - if not any("Program log: Instruction: Create" in log for log in logs): - return None - - # Skip swaps as the first condition may pass them - if any("Program log: Instruction: CreateTokenAccount" in log for log in logs): - return None - - # Find and process program data - for log in logs: - if "Program data:" in log: - try: - encoded_data = log.split(": ")[1] - decoded_data = base64.b64decode(encoded_data) - parsed_data = self._parse_create_instruction(decoded_data) - - if parsed_data and "name" in parsed_data: - mint = Pubkey.from_string(parsed_data["mint"]) - bonding_curve = Pubkey.from_string(parsed_data["bondingCurve"]) - associated_curve = self._find_associated_bonding_curve( - mint, bonding_curve - ) - creator = Pubkey.from_string(parsed_data["creator"]) - creator_vault = self._find_creator_vault(creator) - - return TokenInfo( - name=parsed_data["name"], - symbol=parsed_data["symbol"], - uri=parsed_data["uri"], - mint=mint, - bonding_curve=bonding_curve, - associated_bonding_curve=associated_curve, - user=Pubkey.from_string(parsed_data["user"]), - creator=creator, - creator_vault=creator_vault, - ) - except Exception as e: - logger.error(f"Failed to process log data: {e}") - - return None - - def _parse_create_instruction(self, data: bytes) -> dict | None: - """Parse the create instruction data. - - Args: - data: Raw instruction data - - Returns: - Dictionary of parsed data or None if parsing fails - """ - if len(data) < 8: - return None - - # Check for the correct instruction discriminator - discriminator = struct.unpack(" Pubkey: - """ - Find the associated bonding curve for a given mint and bonding curve. - This uses the standard ATA derivation. - - Args: - mint: Token mint address - bonding_curve: Bonding curve address - - Returns: - Associated bonding curve address - """ - derived_address, _ = Pubkey.find_program_address( - [ - bytes(bonding_curve), - bytes(SystemAddresses.TOKEN_PROGRAM), - bytes(mint), - ], - SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, - ) - return derived_address - - def _find_creator_vault(self, creator: Pubkey) -> Pubkey: - """ - Find the creator vault for a creator. - - Args: - creator: Creator address - - Returns: - Creator vault address - """ - derived_address, _ = Pubkey.find_program_address( - [b"creator-vault", bytes(creator)], - PumpAddresses.PROGRAM, - ) - return derived_address diff --git a/src/monitoring/block_listener.py b/src/monitoring/universal_block_listener.py similarity index 53% rename from src/monitoring/block_listener.py rename to src/monitoring/universal_block_listener.py index 3775e79..fc47448 100644 --- a/src/monitoring/block_listener.py +++ b/src/monitoring/universal_block_listener.py @@ -1,5 +1,5 @@ """ -WebSocket monitoring for pump.fun tokens. +Universal block listener that works with any platform through the interface system. """ import asyncio @@ -7,30 +7,61 @@ import json from collections.abc import Awaitable, Callable import websockets -from solders.pubkey import Pubkey +from interfaces.core import Platform, TokenInfo from monitoring.base_listener import BaseTokenListener -from monitoring.block_event_processor import PumpEventProcessor -from trading.base import TokenInfo +from platforms import get_platform_implementations from utils.logger import get_logger logger = get_logger(__name__) -class BlockListener(BaseTokenListener): - """WebSocket listener for pump.fun token creation events using blockSubscribe.""" +class UniversalBlockListener(BaseTokenListener): + """Universal block listener that works with any platform.""" - def __init__(self, wss_endpoint: str, pump_program: Pubkey): - """Initialize token listener. + def __init__( + self, + wss_endpoint: str, + platforms: list[Platform] | None = None, + ): + """Initialize universal block listener. Args: wss_endpoint: WebSocket endpoint URL - pump_program: Pump.fun program address + platforms: List of platforms to monitor (if None, monitor all supported platforms) """ + super().__init__() self.wss_endpoint = wss_endpoint - self.pump_program = pump_program - self.event_processor = PumpEventProcessor(pump_program) 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: + # We'll need a dummy client for getting the parser + from core.client import SolanaClient + dummy_client = SolanaClient("http://localhost") # Won't be used for parsing + + 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()}") + + except Exception as e: + logger.warning(f"Could not register platform {platform.value}: {e}") async def listen_for_tokens( self, @@ -38,17 +69,21 @@ class BlockListener(BaseTokenListener): match_string: str | None = None, creator_address: str | None = None, ) -> None: - """Listen for new token creations. + """Listen for new token creations using blockSubscribe. Args: token_callback: Callback function for new tokens match_string: Optional string to match in token name/symbol creator_address: Optional creator address to filter by """ + if not self.platform_parsers: + logger.error("No platform parsers available. Cannot listen for tokens.") + return + while True: try: async with websockets.connect(self.wss_endpoint) as websocket: - await self._subscribe_to_program(websocket) + await self._subscribe_to_programs(websocket) ping_task = asyncio.create_task(self._ping_loop(websocket)) try: @@ -58,9 +93,10 @@ class BlockListener(BaseTokenListener): continue logger.info( - f"New token detected: {token_info.name} ({token_info.symbol})" + f"New token detected: {token_info.name} ({token_info.symbol}) on {token_info.platform.value}" ) + # Apply filters if match_string and not ( match_string.lower() in token_info.name.lower() or match_string.lower() in token_info.symbol.lower() @@ -70,10 +106,7 @@ class BlockListener(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..." ) @@ -90,32 +123,35 @@ class BlockListener(BaseTokenListener): logger.info("Reconnecting in 5 seconds...") await asyncio.sleep(5) - async def _subscribe_to_program(self, websocket) -> None: - """Subscribe to blocks mentioning the pump.fun program. + async def _subscribe_to_programs(self, websocket) -> None: + """Subscribe to blocks mentioning any of the monitored program IDs. Args: websocket: Active WebSocket connection """ - subscription_message = json.dumps( - { - "jsonrpc": "2.0", - "id": 1, - "method": "blockSubscribe", - "params": [ - {"mentionsAccountOrProgram": str(self.pump_program)}, - { - "commitment": "confirmed", - "encoding": "base64", # base64 is faster than other encoding options - "showRewards": False, - "transactionDetails": "full", - "maxSupportedTransactionVersion": 0, - }, - ], - } - ) + # For block subscriptions, we can use mentionsAccountOrProgram to monitor multiple programs + # We'll create separate subscriptions for each program to be more specific + for i, program_id in enumerate(self.platform_program_ids): + subscription_message = json.dumps( + { + "jsonrpc": "2.0", + "id": i + 1, + "method": "blockSubscribe", + "params": [ + {"mentionsAccountOrProgram": program_id}, + { + "commitment": "confirmed", + "encoding": "base64", + "showRewards": False, + "transactionDetails": "full", + "maxSupportedTransactionVersion": 0, + }, + ], + } + ) - await websocket.send(subscription_message) - logger.info(f"Subscribed to blocks mentioning program: {self.pump_program}") + await websocket.send(subscription_message) + logger.info(f"Subscribed to blocks mentioning program: {program_id}") async def _ping_loop(self, websocket) -> None: """Keep connection alive with pings. @@ -140,7 +176,7 @@ class BlockListener(BaseTokenListener): logger.error(f"Ping error: {e!s}") async def _wait_for_token_creation(self, websocket) -> TokenInfo | None: - """Wait for token creation event. + """Wait for token creation event from any platform. Args: websocket: Active WebSocket connection @@ -166,15 +202,21 @@ class BlockListener(BaseTokenListener): if "transactions" not in block: return None + # Try each platform's event parser on each transaction for tx in block["transactions"]: if not isinstance(tx, dict) or "transaction" not in tx: continue - token_info = self.event_processor.process_transaction( - tx["transaction"][0] - ) - if token_info: - return token_info + 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 token_info: + return token_info + + return None except TimeoutError: logger.debug("No data received for 30 seconds") @@ -184,4 +226,4 @@ class BlockListener(BaseTokenListener): except Exception as e: logger.error(f"Error processing WebSocket message: {e!s}") - return None + return None \ No newline at end of file diff --git a/src/monitoring/geyser_listener.py b/src/monitoring/universal_geyser_listener.py similarity index 60% rename from src/monitoring/geyser_listener.py rename to src/monitoring/universal_geyser_listener.py index b1ba4ff..0bac6f9 100644 --- a/src/monitoring/geyser_listener.py +++ b/src/monitoring/universal_geyser_listener.py @@ -1,5 +1,5 @@ """ -Geyser monitoring for pump.fun tokens. +Universal Geyser listener that works with any platform through the interface system. """ import asyncio @@ -9,34 +9,36 @@ import grpc from solders.pubkey import Pubkey from geyser.generated import geyser_pb2, geyser_pb2_grpc +from interfaces.core import Platform, TokenInfo from monitoring.base_listener import BaseTokenListener -from monitoring.geyser_event_processor import GeyserEventProcessor -from trading.base import TokenInfo +from platforms import get_platform_implementations from utils.logger import get_logger logger = get_logger(__name__) -class GeyserListener(BaseTokenListener): - """Geyser listener for pump.fun token creation events.""" +class UniversalGeyserListener(BaseTokenListener): + """Universal Geyser listener that works with any platform.""" def __init__( self, geyser_endpoint: str, geyser_api_token: str, geyser_auth_type: str, - pump_program: Pubkey, + platforms: list[Platform] | None = None, ): - """Initialize token listener. + """Initialize universal Geyser listener. Args: geyser_endpoint: Geyser gRPC endpoint URL geyser_api_token: API token for authentication geyser_auth_type: authentication type ('x-token' or 'basic') - pump_program: Pump.fun program address + platforms: List of platforms to monitor (if None, monitor all supported platforms) """ + 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: @@ -44,8 +46,35 @@ class GeyserListener(BaseTokenListener): f"Unsupported auth_type={self.auth_type!r}. " f"Expected one of {valid_auth_types}" ) - self.pump_program = pump_program - self.event_processor = GeyserEventProcessor(pump_program) + + # 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 = set() + + for platform in self.platforms: + try: + # We'll need a dummy client for getting the parser - this is a design issue we should fix + from core.client import SolanaClient + dummy_client = SolanaClient("http://localhost") # Won't be used for parsing + + implementations = get_platform_implementations(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()}") + + 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.""" @@ -66,12 +95,15 @@ class GeyserListener(BaseTokenListener): return geyser_pb2_grpc.GeyserStub(channel), channel def _create_subscription_request(self): - """Create a subscription request for Pump.fun transactions.""" + """Create a subscription request for all monitored platforms.""" request = geyser_pb2.SubscribeRequest() - request.transactions["pump_filter"].account_include.append( - str(self.pump_program) - ) - request.transactions["pump_filter"].failed = False + + # 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 @@ -88,15 +120,18 @@ class GeyserListener(BaseTokenListener): match_string: Optional string to match in token name/symbol creator_address: Optional creator address to filter by """ + if not self.platform_parsers: + logger.error("No platform parsers available. Cannot listen for tokens.") + return + while True: try: stub, channel = await self._create_geyser_connection() request = self._create_subscription_request() logger.info(f"Connected to Geyser endpoint: {self.geyser_endpoint}") - logger.info( - f"Monitoring for transactions involving program: {self.pump_program}" - ) + 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])): @@ -105,9 +140,10 @@ class GeyserListener(BaseTokenListener): continue logger.info( - f"New token detected: {token_info.name} ({token_info.symbol})" + f"New token detected: {token_info.name} ({token_info.symbol}) on {token_info.platform.value}" ) + # Apply filters if match_string and not ( match_string.lower() in token_info.name.lower() or match_string.lower() in token_info.symbol.lower() @@ -156,24 +192,25 @@ class GeyserListener(BaseTokenListener): return None for ix in msg.instructions: - # Skip non-Pump.fun program instructions + # Check which platform this instruction belongs to program_idx = ix.program_id_index if program_idx >= len(msg.account_keys): continue - program_id = msg.account_keys[program_idx] - if bytes(program_id) != bytes(self.pump_program): - continue - - # Process instruction data - token_info = self.event_processor.process_transaction_data( - ix.data, ix.accounts, msg.account_keys - ) - if token_info: - return token_info + 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(): + # Use the platform's event parser + token_info = parser.parse_token_creation_from_instruction( + ix.data, ix.accounts, msg.account_keys + ) + if token_info: + return token_info return None except Exception as e: logger.error(f"Error processing Geyser update: {e}") - return None + return None \ No newline at end of file diff --git a/src/monitoring/logs_listener.py b/src/monitoring/universal_logs_listener.py similarity index 51% rename from src/monitoring/logs_listener.py rename to src/monitoring/universal_logs_listener.py index 4af072e..56c5f03 100644 --- a/src/monitoring/logs_listener.py +++ b/src/monitoring/universal_logs_listener.py @@ -1,5 +1,5 @@ """ -WebSocket monitoring for pump.fun tokens using logsSubscribe. +Universal logs listener that works with any platform through the interface system. """ import asyncio @@ -7,30 +7,61 @@ import json from collections.abc import Awaitable, Callable import websockets -from solders.pubkey import Pubkey +from interfaces.core import Platform, TokenInfo from monitoring.base_listener import BaseTokenListener -from monitoring.logs_event_processor import LogsEventProcessor -from trading.base import TokenInfo +from platforms import get_platform_implementations from utils.logger import get_logger logger = get_logger(__name__) -class LogsListener(BaseTokenListener): - """WebSocket listener for pump.fun token creation events using logsSubscribe.""" +class UniversalLogsListener(BaseTokenListener): + """Universal logs listener that works with any platform.""" - def __init__(self, wss_endpoint: str, pump_program: Pubkey): - """Initialize token listener. + def __init__( + self, + wss_endpoint: str, + platforms: list[Platform] | None = None, + ): + """Initialize universal logs listener. Args: wss_endpoint: WebSocket endpoint URL - pump_program: Pump.fun program address + platforms: List of platforms to monitor (if None, monitor all supported platforms) """ + super().__init__() self.wss_endpoint = wss_endpoint - self.pump_program = pump_program - self.event_processor = LogsEventProcessor(pump_program) 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: + # We'll need a dummy client for getting the parser + from core.client import SolanaClient + dummy_client = SolanaClient("http://localhost") # Won't be used for parsing + + 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()}") + + except Exception as e: + logger.warning(f"Could not register platform {platform.value}: {e}") async def listen_for_tokens( self, @@ -45,6 +76,10 @@ class LogsListener(BaseTokenListener): match_string: Optional string to match in token name/symbol creator_address: Optional creator address to filter by """ + if not self.platform_parsers: + logger.error("No platform parsers available. Cannot listen for tokens.") + return + while True: try: async with websockets.connect(self.wss_endpoint) as websocket: @@ -58,9 +93,10 @@ class LogsListener(BaseTokenListener): continue logger.info( - f"New token detected: {token_info.name} ({token_info.symbol})" + f"New token detected: {token_info.name} ({token_info.symbol}) on {token_info.platform.value}" ) + # Apply filters if match_string and not ( match_string.lower() in token_info.name.lower() or match_string.lower() in token_info.symbol.lower() @@ -70,10 +106,7 @@ class LogsListener(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..." ) @@ -86,38 +119,40 @@ class LogsListener(BaseTokenListener): ping_task.cancel() except Exception as e: - logger.error(f"WebSocket connection error: {str(e)}") + logger.error(f"WebSocket connection error: {e!s}") logger.info("Reconnecting in 5 seconds...") await asyncio.sleep(5) async def _subscribe_to_logs(self, websocket) -> None: - """Subscribe to logs mentioning the pump.fun program. + """Subscribe to logs mentioning any of the monitored program IDs. Args: websocket: Active WebSocket connection """ - subscription_message = json.dumps( - { - "jsonrpc": "2.0", - "id": 1, - "method": "logsSubscribe", - "params": [ - {"mentions": [str(self.pump_program)]}, - {"commitment": "processed"}, - ], - } - ) + # Subscribe to logs for all monitored platforms + for program_id in self.platform_program_ids: + subscription_message = json.dumps( + { + "jsonrpc": "2.0", + "id": len(self.platform_program_ids), # Use different IDs + "method": "logsSubscribe", + "params": [ + {"mentions": [program_id]}, + {"commitment": "processed"}, + ], + } + ) - await websocket.send(subscription_message) - logger.info(f"Subscribed to logs mentioning program: {self.pump_program}") + await websocket.send(subscription_message) + logger.info(f"Subscribed to logs mentioning program: {program_id}") - # Wait for subscription confirmation - response = await websocket.recv() - response_data = json.loads(response) - if "result" in response_data: - logger.info(f"Subscription confirmed with ID: {response_data['result']}") - else: - logger.warning(f"Unexpected subscription response: {response}") + # Wait for subscription confirmation + response = await websocket.recv() + response_data = json.loads(response) + if "result" in response_data: + logger.info(f"Subscription confirmed with ID: {response_data['result']}") + else: + logger.warning(f"Unexpected subscription response: {response}") async def _ping_loop(self, websocket) -> None: """Keep connection alive with pings. @@ -131,7 +166,7 @@ class LogsListener(BaseTokenListener): try: pong_waiter = await websocket.ping() await asyncio.wait_for(pong_waiter, timeout=10) - except asyncio.TimeoutError: + except TimeoutError: logger.warning("Ping timeout - server not responding") # Force reconnection await websocket.close() @@ -139,9 +174,17 @@ class LogsListener(BaseTokenListener): except asyncio.CancelledError: pass except Exception as e: - logger.error(f"Ping error: {str(e)}") + logger.error(f"Ping error: {e!s}") async def _wait_for_token_creation(self, websocket) -> TokenInfo | None: + """Wait for token creation events from any platform. + + Args: + websocket: Active WebSocket connection + + Returns: + TokenInfo if a token creation is found, None otherwise + """ try: response = await asyncio.wait_for(websocket.recv(), timeout=30) data = json.loads(response) @@ -153,15 +196,20 @@ class LogsListener(BaseTokenListener): logs = log_data.get("logs", []) signature = log_data.get("signature", "unknown") - # Use the processor to extract token info - return self.event_processor.process_program_logs(logs, signature) + # Try each platform's event parser + for platform, parser in self.platform_parsers.items(): + token_info = parser.parse_token_creation_from_logs(logs, signature) + if token_info: + return token_info - except asyncio.TimeoutError: + return None + + except TimeoutError: logger.debug("No data received for 30 seconds") except websockets.exceptions.ConnectionClosed: logger.warning("WebSocket connection closed") raise except Exception as e: - logger.error(f"Error processing WebSocket message: {str(e)}") + logger.error(f"Error processing WebSocket message: {e!s}") - return None + return None \ No newline at end of file diff --git a/src/platforms/__init__.py b/src/platforms/__init__.py index ea584fb..9348112 100644 --- a/src/platforms/__init__.py +++ b/src/platforms/__init__.py @@ -6,7 +6,7 @@ platform-specific implementations of the trading interfaces. """ from dataclasses import dataclass -from typing import Any, Dict, Optional, Type +from typing import Any from core.client import SolanaClient from interfaces.core import ( @@ -17,8 +17,6 @@ from interfaces.core import ( Platform, ) -from . import letsbonk, pumpfun - @dataclass class PlatformImplementations: @@ -145,21 +143,45 @@ class PlatformFactory: self._setup_default_platforms() def _setup_default_platforms(self) -> None: - """Setup default platform registrations. + """Setup default platform registrations.""" + # Import platform implementations dynamically to avoid circular imports + try: + from platforms.pumpfun import ( + PumpFunAddressProvider, + PumpFunCurveManager, + PumpFunEventParser, + PumpFunInstructionBuilder, + ) + + self.registry.register_platform( + Platform.PUMP_FUN, + PumpFunAddressProvider, + PumpFunInstructionBuilder, + PumpFunCurveManager, + PumpFunEventParser + ) + + except ImportError as e: + print(f"Warning: Could not register pump.fun platform: {e}") - This will be populated as we implement each platform. - """ - # Platforms will be registered here as they are implemented - # Example: - # from platforms.pumpfun import PumpFunAddressProvider, PumpFunInstructionBuilder, ... - # self.registry.register_platform( - # Platform.PUMP_FUN, - # PumpFunAddressProvider, - # PumpFunInstructionBuilder, - # PumpFunCurveManager, - # PumpFunEventParser - # ) - pass + try: + from platforms.letsbonk import ( + LetsBonkAddressProvider, + LetsBonkCurveManager, + LetsBonkEventParser, + LetsBonkInstructionBuilder, + ) + + self.registry.register_platform( + Platform.LETS_BONK, + LetsBonkAddressProvider, + LetsBonkInstructionBuilder, + LetsBonkCurveManager, + LetsBonkEventParser + ) + + except ImportError as e: + print(f"Warning: Could not register LetsBonk platform: {e}") def create_for_platform( self, diff --git a/src/platforms/letsbonk/__init__.py b/src/platforms/letsbonk/__init__.py index 8e494e8..60c1cfc 100644 --- a/src/platforms/letsbonk/__init__.py +++ b/src/platforms/letsbonk/__init__.py @@ -1,27 +1,15 @@ """ -LetsBonk platform registration and exports. +LetsBonk platform exports. -This module registers all LetsBonk implementations with the platform factory -and provides convenient imports for the LetsBonk platform. +This module provides convenient imports for the LetsBonk platform implementations. +Platform registration is now handled by the main platform factory. """ -from interfaces.core import Platform -from platforms import register_platform_implementations - from .address_provider import LetsBonkAddressProvider from .curve_manager import LetsBonkCurveManager from .event_parser import LetsBonkEventParser from .instruction_builder import LetsBonkInstructionBuilder -# Register LetsBonk platform implementations -register_platform_implementations( - Platform.LETS_BONK, - LetsBonkAddressProvider, - LetsBonkInstructionBuilder, - LetsBonkCurveManager, - LetsBonkEventParser -) - # Export implementations for direct use if needed __all__ = [ 'LetsBonkAddressProvider', diff --git a/src/platforms/pumpfun/__init__.py b/src/platforms/pumpfun/__init__.py index 38c8f53..bc31eb3 100644 --- a/src/platforms/pumpfun/__init__.py +++ b/src/platforms/pumpfun/__init__.py @@ -1,27 +1,15 @@ """ -Pump.Fun platform registration and exports. +Pump.Fun platform exports. -This module registers all pump.fun implementations with the platform factory -and provides convenient imports for the pump.fun platform. +This module provides convenient imports for the pump.fun platform implementations. +Platform registration is now handled by the main platform factory. """ -from interfaces.core import Platform -from platforms import register_platform_implementations - from .address_provider import PumpFunAddressProvider from .curve_manager import PumpFunCurveManager from .event_parser import PumpFunEventParser from .instruction_builder import PumpFunInstructionBuilder -# Register pump.fun platform implementations -register_platform_implementations( - Platform.PUMP_FUN, - PumpFunAddressProvider, - PumpFunInstructionBuilder, - PumpFunCurveManager, - PumpFunEventParser -) - # Export implementations for direct use if needed __all__ = [ 'PumpFunAddressProvider', diff --git a/src/trading/base.py b/src/trading/base.py index ee7eb63..e2cb83d 100644 --- a/src/trading/base.py +++ b/src/trading/base.py @@ -12,13 +12,76 @@ from typing import Any from solders.pubkey import Pubkey -from interfaces.core import Platform - -# Import the new enhanced TokenInfo and Platform from interfaces -from interfaces.core import TokenInfo as EnhancedTokenInfo +# Import from interfaces to avoid duplication +from interfaces.core import Platform, TokenInfo -# Keep the original TokenInfo structure for backward compatibility +@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 + error_message: str | None = None + amount: float | None = None + price: float | None = None + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for logging/serialization. + + Returns: + Dictionary representation of the trade result + """ + return { + "success": self.success, + "platform": self.platform.value, + "tx_signature": self.tx_signature, + "error_message": self.error_message, + "amount": self.amount, + "price": self.price, + } + + +class Trader(ABC): + """Enhanced base interface for trading operations with platform support.""" + + @abstractmethod + async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult: + """Execute trading operation. + + Args: + token_info: Enhanced token information with platform support + + Returns: + TradeResult with operation outcome including platform info + """ + pass + + 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: + token_info: Enhanced token information + + Returns: + List of relevant accounts (basic implementation) + """ + # 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 + + +# Legacy TokenInfo for backward compatibility (keep pump.fun specific) @dataclass class TokenInfo_Legacy: """Legacy token information structure for backward compatibility.""" @@ -73,86 +136,6 @@ class TokenInfo_Legacy: } -@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 - error_message: str | None = None - amount: float | None = None - price: float | None = None - - def to_dict(self) -> dict[str, Any]: - """Convert to dictionary for logging/serialization. - - Returns: - Dictionary representation of the trade result - """ - return { - "success": self.success, - "platform": self.platform.value, - "tx_signature": self.tx_signature, - "error_message": self.error_message, - "amount": self.amount, - "price": self.price, - } - - -class Trader(ABC): - """Enhanced base interface for trading operations with platform support.""" - - @abstractmethod - async def execute(self, token_info: "TokenInfo", *args, **kwargs) -> TradeResult: - """Execute trading operation. - - Args: - token_info: Enhanced token information with platform support - - Returns: - TradeResult with operation outcome including platform info - """ - pass - - 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: - token_info: Enhanced token information - - Returns: - List of relevant accounts (default implementation for pump.fun compatibility) - """ - # Default implementation maintains pump.fun compatibility - from core.pubkeys import PumpAddresses - - 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) - - # Add platform program - if token_info.platform == Platform.PUMP_FUN: - accounts.extend([ - PumpAddresses.PROGRAM, - PumpAddresses.FEE, - ]) - # Other platforms would add their specific accounts here - - return accounts - - -# Use the enhanced TokenInfo as the main TokenInfo class -# This provides the new functionality while maintaining the same import path -TokenInfo = EnhancedTokenInfo - - def upgrade_token_info(legacy_token_info: TokenInfo_Legacy) -> TokenInfo: """Convert legacy TokenInfo to enhanced TokenInfo. @@ -246,18 +229,11 @@ def create_pump_fun_token_info( Returns: Enhanced TokenInfo configured for pump.fun """ - from core.pubkeys import PumpAddresses - - # Default creator to user if not provided - if creator is None: - creator = user - - # Derive creator vault if not provided - if creator_vault is None: - creator_vault, _ = Pubkey.find_program_address( - [b"creator-vault", bytes(creator)], - PumpAddresses.PROGRAM, - ) + # Derive creator vault if not provided (import here to avoid circular imports) + if creator_vault is None and creator: + # 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, @@ -268,7 +244,7 @@ def create_pump_fun_token_info( bonding_curve=bonding_curve, associated_bonding_curve=associated_bonding_curve, user=user, - creator=creator, + creator=creator or user, creator_vault=creator_vault, **kwargs ) @@ -303,10 +279,6 @@ def create_lets_bonk_token_info( Returns: Enhanced TokenInfo configured for LetsBonk """ - # Default creator to user if not provided - if creator is None: - creator = user - return TokenInfo( name=name, symbol=symbol, @@ -317,7 +289,7 @@ def create_lets_bonk_token_info( base_vault=base_vault, quote_vault=quote_vault, user=user, - creator=creator, + creator=creator or user, **kwargs ) @@ -406,7 +378,6 @@ def validate_token_info(token_info: TokenInfo) -> bool: # Backward compatibility exports -# This allows existing imports to continue working __all__ = [ 'Platform', # Platform enum 'TokenInfo', # Enhanced TokenInfo (main export) @@ -415,13 +386,10 @@ __all__ = [ 'Trader', # Enhanced Trader base class 'create_legacy_token_info', 'create_lets_bonk_token_info', - # Convenience functions 'create_pump_fun_token_info', 'get_platform_specific_fields', 'is_lets_bonk_token', - # Utility functions 'is_pump_fun_token', - # Conversion functions 'upgrade_token_info', 'validate_token_info', ] \ No newline at end of file diff --git a/src/trading/buyer.py b/src/trading/buyer.py deleted file mode 100644 index 12dc3f4..0000000 --- a/src/trading/buyer.py +++ /dev/null @@ -1,416 +0,0 @@ -""" -Buy operations for pump.fun tokens. -""" - -import struct -from typing import Final - -from solders.instruction import AccountMeta, Instruction -from solders.pubkey import Pubkey -from spl.token.instructions import create_idempotent_associated_token_account - -from core.client import SolanaClient -from core.curve import BondingCurveManager -from core.priority_fee.manager import PriorityFeeManager -from core.pubkeys import ( - LAMPORTS_PER_SOL, - TOKEN_DECIMALS, - PumpAddresses, - SystemAddresses, -) -from core.wallet import Wallet -from trading.base import TokenInfo, Trader, TradeResult -from utils.logger import get_logger - -logger = get_logger(__name__) - -# Discriminator for the buy instruction -EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(" TradeResult: - """Execute buy operation. - - Args: - token_info: Token information - - Returns: - TradeResult with buy outcome - """ - try: - # Convert amount to lamports - amount_lamports = int(self.amount * LAMPORTS_PER_SOL) - - if self.extreme_fast_mode: - # Skip the wait and directly calculate the amount - token_amount = self.extreme_fast_token_amount - token_price_sol = self.amount / token_amount - # logger.info(f"EXTREME FAST Mode: Buying {token_amount} tokens.") - else: - # Regular behavior with RPC call - curve_state = await self.curve_manager.get_curve_state( - token_info.bonding_curve - ) - token_price_sol = curve_state.calculate_price() - token_amount = self.amount / token_price_sol - - # Calculate maximum SOL to spend with slippage - max_amount_lamports = int(amount_lamports * (1 + self.slippage)) - - associated_token_account = self.wallet.get_associated_token_address( - token_info.mint - ) - - tx_signature = await self._send_buy_transaction( - token_info, - associated_token_account, - token_amount, - max_amount_lamports, - ) - - logger.info( - f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token" - ) - logger.info( - f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)" - ) - - success = await self.client.confirm_transaction(tx_signature) - - if success: - # Get actual execution data from bonding curve balance changes - actual_price, actual_tokens = await self._get_actual_execution_price( - tx_signature, token_info - ) - - logger.info(f"Buy transaction confirmed: {tx_signature}") - logger.info( - f"Actual price paid to bonding curve: {actual_price:.8f} SOL per token" - ) - - return TradeResult( - success=True, - tx_signature=tx_signature, - amount=actual_tokens, # Actual tokens received - price=actual_price, # Actual price based on bonding curve SOL flow - ) - else: - return TradeResult( - success=False, - error_message=f"Transaction failed to confirm: {tx_signature}", - ) - - except Exception as e: - logger.error(f"Buy operation failed: {e!s}") - return TradeResult(success=False, error_message=str(e)) - - async def _send_buy_transaction( - self, - token_info: TokenInfo, - associated_token_account: Pubkey, - token_amount: float, - max_amount_lamports: int, - ) -> str: - """Send buy transaction. - - Args: - token_info: Token information - associated_token_account: User's token account - token_amount: Amount of tokens to buy - max_amount_lamports: Maximum SOL to spend in lamports - - Returns: - Transaction signature - - Raises: - Exception: If transaction fails after all retries - """ - accounts = [ - AccountMeta( - pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False - ), - AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True), - AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), - AccountMeta( - pubkey=token_info.bonding_curve, is_signer=False, is_writable=True - ), - AccountMeta( - pubkey=token_info.associated_bonding_curve, - is_signer=False, - is_writable=True, - ), - AccountMeta( - pubkey=associated_token_account, is_signer=False, is_writable=True - ), - AccountMeta(pubkey=self.wallet.pubkey, is_signer=True, is_writable=True), - AccountMeta( - pubkey=SystemAddresses.PROGRAM, is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=token_info.creator_vault, is_signer=False, is_writable=True - ), - AccountMeta( - pubkey=PumpAddresses.EVENT_AUTHORITY, is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=PumpAddresses.PROGRAM, is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=PumpAddresses.find_global_volume_accumulator(), is_signer=False, is_writable=True - ), - AccountMeta( - pubkey=PumpAddresses.find_user_volume_accumulator(self.wallet.pubkey), is_signer=False, is_writable=True - ), - ] - - # Prepare idempotent create ATA instruction: it will not fail if ATA already exists - idempotent_ata_ix = create_idempotent_associated_token_account( - self.wallet.pubkey, - self.wallet.pubkey, - token_info.mint, - SystemAddresses.TOKEN_PROGRAM, - ) - - # Prepare buy instruction data - token_amount_raw = int(token_amount * 10**TOKEN_DECIMALS) - data = ( - EXPECTED_DISCRIMINATOR - + struct.pack(" tuple[float, float]: - """Get actual execution price from bonding curve SOL balance changes.""" - try: - client = await self.client.get_client() - - tx_response = await client.get_transaction( - tx_signature, - encoding="jsonParsed", - commitment="confirmed", - max_supported_transaction_version=0, - ) - - if not tx_response.value or not tx_response.value.transaction: - raise ValueError("Transaction not found") - - meta = tx_response.value.transaction.meta - if not meta or not meta.pre_balances or not meta.post_balances: - raise ValueError("Transaction balance data not found") - - # Get accounts - they're ParsedAccountTxStatus objects, need to extract pubkey - accounts = tx_response.value.transaction.transaction.message.account_keys - - # Find bonding curve account index in the transaction - bonding_curve_index = None - for i, account in enumerate(accounts): - # Extract pubkey from ParsedAccountTxStatus object - account_pubkey = ( - str(account.pubkey) if hasattr(account, "pubkey") else str(account) - ) - - if account_pubkey == str(token_info.bonding_curve): - bonding_curve_index = i - break - - if bonding_curve_index is None: - raise ValueError("Bonding curve not found in transaction accounts") - - pre_balance_lamports = meta.pre_balances[bonding_curve_index] - post_balance_lamports = meta.post_balances[bonding_curve_index] - - sol_sent_to_curve = ( - post_balance_lamports - pre_balance_lamports - ) / LAMPORTS_PER_SOL - - if sol_sent_to_curve <= 0: - raise ValueError(f"No SOL sent to bonding curve: {sol_sent_to_curve}") - - tokens_received = await self._get_tokens_received_from_tx( - tx_response, token_info - ) - - if tokens_received == 0: - raise ValueError("Cannot compute execution price: zero tokens received") - actual_price = sol_sent_to_curve / tokens_received - - logger.info(f"Bonding curve received: {sol_sent_to_curve:.6f} SOL") - logger.info(f"We received: {tokens_received:.6f} tokens") - logger.info(f"Actual execution price: {actual_price:.8f} SOL per token") - - return actual_price, tokens_received - - except Exception as e: - logger.warning( - f"Failed to get actual execution price from bonding curve: {e}" - ) - # Fallback to EXTREME_FAST estimate - tokens_received = ( - self.extreme_fast_token_amount - if self.extreme_fast_mode - else self.amount - / await self.curve_manager.calculate_price(token_info.bonding_curve) - ) - if tokens_received == 0: - logger.error("Fallback failed – unable to determine tokens received") - return 0.0, 0.0 - return self.amount / tokens_received, tokens_received - - async def _get_tokens_received_from_tx( - self, tx_response, token_info: TokenInfo - ) -> float: - """Extract tokens received from transaction token balance changes.""" - meta = tx_response.value.transaction.meta - - pre_token_balance = 0 - post_token_balance = 0 - - wallet_str = str(self.wallet.pubkey) - mint_str = str(token_info.mint) - - if meta.pre_token_balances: - for balance in meta.pre_token_balances: - # Convert to string for comparison - balance_owner = ( - str(balance.owner) - if hasattr(balance, "owner") - else str(getattr(balance, "owner", "")) - ) - balance_mint = ( - str(balance.mint) - if hasattr(balance, "mint") - else str(getattr(balance, "mint", "")) - ) - - if balance_owner == wallet_str and balance_mint == mint_str: - try: - # Try multiple ways to get the amount - if hasattr(balance, "ui_token_amount"): - amount_obj = balance.ui_token_amount - if ( - hasattr(amount_obj, "amount") - and amount_obj.amount is not None - ): - pre_token_balance = int(amount_obj.amount) - elif ( - hasattr(amount_obj, "ui_amount") - and amount_obj.ui_amount is not None - ): - pre_token_balance = int( - float(amount_obj.ui_amount) * (10**TOKEN_DECIMALS) - ) - except (ValueError, TypeError) as e: - logger.warning(f"Error parsing pre-token balance: {e}") - break - - # Check post-token balances - if meta.post_token_balances: - for balance in meta.post_token_balances: - # Convert to string for comparison - balance_owner = ( - str(balance.owner) - if hasattr(balance, "owner") - else str(getattr(balance, "owner", "")) - ) - balance_mint = ( - str(balance.mint) - if hasattr(balance, "mint") - else str(getattr(balance, "mint", "")) - ) - - if balance_owner == wallet_str and balance_mint == mint_str: - try: - # Try multiple ways to get the amount - if hasattr(balance, "ui_token_amount"): - amount_obj = balance.ui_token_amount - if ( - hasattr(amount_obj, "amount") - and amount_obj.amount is not None - ): - post_token_balance = int(amount_obj.amount) - elif ( - hasattr(amount_obj, "ui_amount") - and amount_obj.ui_amount is not None - ): - post_token_balance = int( - float(amount_obj.ui_amount) * (10**TOKEN_DECIMALS) - ) - except (ValueError, TypeError) as e: - logger.warning(f"Error parsing post-token balance: {e}") - break - - # Calculate tokens received - if pre_token_balance == 0 and post_token_balance > 0: - tokens_received_raw = post_token_balance - else: - tokens_received_raw = post_token_balance - pre_token_balance - - if tokens_received_raw <= 0: - logger.warning( - "Token balance search failed. Using fallback from EXTREME_FAST estimate." - ) - # Fallback: use the amount we know we bought - if self.extreme_fast_mode and self.extreme_fast_token_amount > 0: - return self.extreme_fast_token_amount - else: - logger.error("Cannot determine tokens received from transaction") - return 0.0 - - return tokens_received_raw / 10**TOKEN_DECIMALS diff --git a/src/trading/seller.py b/src/trading/seller.py deleted file mode 100644 index ac8b3e5..0000000 --- a/src/trading/seller.py +++ /dev/null @@ -1,213 +0,0 @@ -""" -Sell operations for pump.fun tokens. -""" - -import struct -from typing import Final - -from solders.instruction import AccountMeta, Instruction -from solders.pubkey import Pubkey - -from core.client import SolanaClient -from core.curve import BondingCurveManager -from core.priority_fee.manager import PriorityFeeManager -from core.pubkeys import ( - LAMPORTS_PER_SOL, - TOKEN_DECIMALS, - PumpAddresses, - SystemAddresses, -) -from core.wallet import Wallet -from trading.base import TokenInfo, Trader, TradeResult -from utils.logger import get_logger - -logger = get_logger(__name__) - -# Discriminator for the sell instruction -EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack(" TradeResult: - """Execute sell operation. - - Args: - token_info: Token information - - Returns: - TradeResult with sell outcome - """ - try: - # Get associated token account - associated_token_account = self.wallet.get_associated_token_address( - token_info.mint - ) - - # Get token balance - token_balance = await self.client.get_token_account_balance( - associated_token_account - ) - token_balance_decimal = token_balance / 10**TOKEN_DECIMALS - - logger.info(f"Token balance: {token_balance_decimal}") - - if token_balance == 0: - logger.info("No tokens to sell.") - return TradeResult(success=False, error_message="No tokens to sell") - - # Fetch token price - curve_state = await self.curve_manager.get_curve_state( - token_info.bonding_curve - ) - token_price_sol = curve_state.calculate_price() - - logger.info(f"Price per Token: {token_price_sol:.8f} SOL") - - # Calculate minimum SOL output with slippage - amount = token_balance - expected_sol_output = float(token_balance_decimal) * float(token_price_sol) - slippage_factor = 1 - self.slippage - min_sol_output = int( - (expected_sol_output * slippage_factor) * LAMPORTS_PER_SOL - ) - - logger.info(f"Selling {token_balance_decimal} tokens") - logger.info(f"Expected SOL output: {expected_sol_output:.8f} SOL") - logger.info( - f"Minimum SOL output (with {self.slippage * 100}% slippage): {min_sol_output / LAMPORTS_PER_SOL:.8f} SOL" - ) - - tx_signature = await self._send_sell_transaction( - token_info, - associated_token_account, - amount, - min_sol_output, - ) - - success = await self.client.confirm_transaction(tx_signature) - - if success: - logger.info(f"Sell transaction confirmed: {tx_signature}") - return TradeResult( - success=True, - tx_signature=tx_signature, - amount=token_balance_decimal, - price=token_price_sol, - ) - else: - return TradeResult( - success=False, - error_message=f"Transaction failed to confirm: {tx_signature}", - ) - - except Exception as e: - logger.error(f"Sell operation failed: {e!s}") - return TradeResult(success=False, error_message=str(e)) - - async def _send_sell_transaction( - self, - token_info: TokenInfo, - associated_token_account: Pubkey, - token_amount: int, - min_sol_output: int, - ) -> str: - """Send sell transaction. - - Args: - mint: Token information - associated_token_account: User's token account - token_amount: Amount of tokens to sell in raw units - min_sol_output: Minimum SOL to receive in lamports - - Returns: - Transaction signature - - Raises: - Exception: If transaction fails after all retries - """ - # Prepare sell instruction accounts - accounts = [ - AccountMeta( - pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False - ), - AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True), - AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), - AccountMeta( - pubkey=token_info.bonding_curve, is_signer=False, is_writable=True - ), - AccountMeta( - pubkey=token_info.associated_bonding_curve, - is_signer=False, - is_writable=True, - ), - AccountMeta( - pubkey=associated_token_account, is_signer=False, is_writable=True - ), - AccountMeta(pubkey=self.wallet.pubkey, is_signer=True, is_writable=True), - AccountMeta( - pubkey=SystemAddresses.PROGRAM, is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=token_info.creator_vault, - is_signer=False, - is_writable=True, - ), - AccountMeta( - pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=PumpAddresses.EVENT_AUTHORITY, is_signer=False, is_writable=False - ), - AccountMeta( - pubkey=PumpAddresses.PROGRAM, is_signer=False, is_writable=False - ), - ] - - # Prepare sell instruction data - data = ( - EXPECTED_DISCRIMINATOR - + struct.pack(" None: """Start the trading bot and listen for new tokens.""" - logger.info("Starting pump.fun trader") - logger.info( - f"Match filter: {self.match_string if self.match_string else 'None'}" - ) - logger.info( - f"Creator filter: {self.bro_address if self.bro_address else 'None'}" - ) + logger.info(f"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"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: @@ -262,22 +218,16 @@ class PumpTrader: # 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: @@ -300,7 +250,7 @@ class PumpTrader: finally: await self._cleanup_resources() - logger.info("Pump trader has shut down") + logger.info("Universal Trader has shut down") async def _wait_for_token(self) -> TokenInfo | None: """Wait for a single token to be detected. @@ -334,16 +284,12 @@ class PumpTrader: # 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() @@ -391,7 +337,7 @@ class PumpTrader: 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})") + 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.""" @@ -402,21 +348,15 @@ class PumpTrader: # 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: @@ -435,19 +375,19 @@ class PumpTrader: token_info: Token information """ try: - # Wait for bonding curve to stabilize (unless in extreme fast mode) + # 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}") + return + + # Wait for pool/curve to stabilize (unless in extreme fast mode) if not self.extreme_fast_mode: - # Save token info to file - # await self._save_token_info(token_info) - logger.info( - f"Waiting for {self.wait_time_after_creation} seconds for the bonding curve to stabilize..." - ) + await self._save_token_info(token_info) + 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}..." - ) + 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: @@ -457,31 +397,21 @@ class PumpTrader: # 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 as e: logger.error(f"Error handling token {token_info.symbol}: {e!s}") - async def _handle_successful_buy( - self, token_info: TokenInfo, buy_result: TradeResult - ) -> None: + async def _handle_successful_buy(self, token_info: TokenInfo, buy_result: TradeResult) -> None: """Handle successful token purchase. Args: token_info: Token information buy_result: The result of the buy operation """ - logger.info(f"Successfully bought {token_info.symbol}") - self._log_trade( - "buy", - token_info, - buy_result.price, # type: ignore - buy_result.amount, # type: ignore - buy_result.tx_signature, - ) + 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 @@ -495,9 +425,7 @@ class PumpTrader: 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. Args: @@ -516,9 +444,7 @@ class PumpTrader: 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. Args: @@ -529,8 +455,8 @@ class PumpTrader: position = Position.create_from_buy_result( mint=token_info.mint, symbol=token_info.symbol, - entry_price=buy_result.price, # type: ignore - quantity=buy_result.amount, # type: ignore + entry_price=buy_result.price, + quantity=buy_result.amount, take_profit_percentage=self.take_profit_percentage, stop_loss_percentage=self.stop_loss_percentage, max_hold_time=self.max_hold_time, @@ -559,13 +485,7 @@ class PumpTrader: if sell_result.success: logger.info(f"Successfully sold {token_info.symbol}") - self._log_trade( - "sell", - token_info, - sell_result.price, # type: ignore - sell_result.amount, # type: ignore - sell_result.tx_signature, - ) + 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, @@ -577,29 +497,25 @@ class PumpTrader: 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. Args: token_info: Token information position: Position to monitor """ - 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 + pool_address = self._get_pool_address(token_info) + curve_manager = self.platform_implementations.curve_manager while position.is_active: try: - # Get current price from bonding curve - current_price = await self.curve_manager.calculate_price( - token_info.bonding_curve - ) + # Get current price from pool/curve + current_price = await curve_manager.calculate_price(pool_address) # Check if position should be exited should_exit, exit_reason = position.should_exit(current_price) @@ -610,33 +526,21 @@ class PumpTrader: # 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) if sell_result.success: # Close position with actual exit price - position.close_position(sell_result.price, exit_reason) # type: ignore + 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, # type: ignore - sell_result.amount, # type: ignore - 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( @@ -649,27 +553,40 @@ class PumpTrader: 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 as e: logger.error(f"Error monitoring position: {e}") - 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. + + Args: + token_info: Token information + + Returns: + Pool/curve address + """ + address_provider = self.platform_implementations.address_provider + + if token_info.platform == Platform.PUMP_FUN: + return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint) + elif token_info.platform == Platform.LETS_BONK: + return token_info.pool_state or address_provider.derive_pool_address(token_info.mint) + else: + # Fallback to deriving the address + return address_provider.derive_pool_address(token_info.mint) async def _save_token_info(self, token_info: TokenInfo) -> None: """Save token information to a file. @@ -681,21 +598,32 @@ class PumpTrader: os.makedirs("trades", exist_ok=True) file_name = os.path.join("trades", f"{token_info.mint}.txt") + # Convert to dictionary for saving + token_dict = { + "name": token_info.name, + "symbol": token_info.symbol, + "uri": token_info.uri, + "mint": str(token_info.mint), + "platform": token_info.platform.value, + "user": str(token_info.user) if token_info.user else None, + "creator": str(token_info.creator) if token_info.creator else None, + # Platform-specific fields + "bonding_curve": str(token_info.bonding_curve) if token_info.bonding_curve else None, + "associated_bonding_curve": str(token_info.associated_bonding_curve) if token_info.associated_bonding_curve else None, + "creator_vault": str(token_info.creator_vault) if token_info.creator_vault else None, + "pool_state": str(token_info.pool_state) if token_info.pool_state else None, + "base_vault": str(token_info.base_vault) if token_info.base_vault else None, + "quote_vault": str(token_info.quote_vault) if token_info.quote_vault else None, + } + with open(file_name, "w") as file: - file.write(json.dumps(token_info.to_dict(), indent=2)) + file.write(json.dumps(token_dict, indent=2)) logger.info(f"Token information saved to {file_name}") except Exception as e: logger.error(f"Failed to save token information: {e!s}") - def _log_trade( - self, - action: str, - token_info: TokenInfo, - price: float, - amount: float, - tx_hash: str | None, - ) -> None: + def _log_trade(self, action: str, token_info: TokenInfo, price: float, amount: float, tx_hash: str | None) -> None: """Log trade information. Args: @@ -711,6 +639,7 @@ class PumpTrader: log_entry = { "timestamp": datetime.utcnow().isoformat(), "action": action, + "platform": token_info.platform.value, "token_address": str(token_info.mint), "symbol": token_info.symbol, "price": price, @@ -722,3 +651,7 @@ class PumpTrader: log_file.write(json.dumps(log_entry) + "\n") except Exception as e: logger.error(f"Failed to log trade information: {e!s}") + + +# Backward compatibility alias +PumpTrader = UniversalTrader # Legacy name for backward compatibility \ No newline at end of file From 227ef0b3bdcbb41cc01f740e79af109c1b3a1204 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 13:27:07 +0000 Subject: [PATCH 03/30] wip(core): platform aware trading --- src/bot_runner.py | 236 +++++++++------ src/config_loader.py | 317 +++++++++++--------- src/core/curve.py | 165 ++++++++++ src/monitoring/universal_geyser_listener.py | 52 ++-- src/monitoring/universal_logs_listener.py | 32 +- src/platforms/__init__.py | 24 +- src/platforms/letsbonk/event_parser.py | 135 +++++++-- src/trading/platform_aware.py | 103 ++----- src/trading/universal_trader.py | 141 +++------ 9 files changed, 699 insertions(+), 506 deletions(-) create mode 100644 src/core/curve.py diff --git a/src/bot_runner.py b/src/bot_runner.py index 0dc79da..ba18588 100644 --- a/src/bot_runner.py +++ b/src/bot_runner.py @@ -12,18 +12,14 @@ from config_loader import ( get_platform_from_config, load_bot_config, print_config_summary, + validate_platform_listener_combination, ) from trading.universal_trader import UniversalTrader from utils.logger import setup_file_logging def setup_logging(bot_name: str): - """ - Set up logging to file for a specific bot instance. - - Args: - bot_name: Name of the bot for the log file - """ + """Set up logging to file for a specific bot instance.""" log_dir = Path("logs") log_dir.mkdir(exist_ok=True) @@ -34,96 +30,119 @@ def setup_logging(bot_name: str): async def start_bot(config_path: str): - """ - Start a trading bot with the configuration from the specified path. - - Args: - config_path: Path to the YAML configuration file - """ + """Start a trading bot with the configuration from the specified path.""" cfg = load_bot_config(config_path) setup_logging(cfg["name"]) print_config_summary(cfg) - # Get platform from configuration - platform = get_platform_from_config(cfg) - - # Initialize universal trader with platform-specific configuration - trader = UniversalTrader( - # Connection settings - rpc_endpoint=cfg["rpc_endpoint"], - wss_endpoint=cfg["wss_endpoint"], - private_key=cfg["private_key"], - - # Platform configuration - platform=platform, - - # Trade parameters - buy_amount=cfg["trade"]["buy_amount"], - buy_slippage=cfg["trade"]["buy_slippage"], - sell_slippage=cfg["trade"]["sell_slippage"], - - # Extreme fast mode settings - extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False), - extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30), - - # Exit strategy configuration - exit_strategy=cfg["trade"].get("exit_strategy", "time_based"), - take_profit_percentage=cfg["trade"].get("take_profit_percentage"), - stop_loss_percentage=cfg["trade"].get("stop_loss_percentage"), - max_hold_time=cfg["trade"].get("max_hold_time"), - price_check_interval=cfg["trade"].get("price_check_interval", 10), - - # Listener configuration - listener_type=cfg["filters"]["listener_type"], - - # Geyser configuration (if applicable) - geyser_endpoint=cfg.get("geyser", {}).get("endpoint"), - geyser_api_token=cfg.get("geyser", {}).get("api_token"), - geyser_auth_type=cfg.get("geyser", {}).get("auth_type", "x-token"), - - # PumpPortal configuration (if applicable) - pumpportal_url=cfg.get("pumpportal", {}).get( - "url", "wss://pumpportal.fun/api/data" - ), - - # Priority fee configuration - enable_dynamic_priority_fee=cfg.get("priority_fees", {}).get( - "enable_dynamic", False - ), - enable_fixed_priority_fee=cfg.get("priority_fees", {}).get( - "enable_fixed", True - ), - fixed_priority_fee=cfg.get("priority_fees", {}).get("fixed_amount", 500000), - extra_priority_fee=cfg.get("priority_fees", {}).get("extra_percentage", 0.0), - hard_cap_prior_fee=cfg.get("priority_fees", {}).get("hard_cap", 500000), - - # Retry and timeout settings - max_retries=cfg.get("retries", {}).get("max_attempts", 10), - wait_time_after_creation=cfg.get("retries", {}).get("wait_after_creation", 15), - wait_time_after_buy=cfg.get("retries", {}).get("wait_after_buy", 15), - wait_time_before_new_token=cfg.get("retries", {}).get( - "wait_before_new_token", 15 - ), - max_token_age=cfg.get("filters", {}).get("max_token_age", 0.001), - token_wait_timeout=cfg.get("timing", {}).get("token_wait_timeout", 120), - - # Cleanup settings - cleanup_mode=cfg.get("cleanup", {}).get("mode", "disabled"), - cleanup_force_close_with_burn=cfg.get("cleanup", {}).get( - "force_close_with_burn", False - ), - cleanup_with_priority_fee=cfg.get("cleanup", {}).get( - "with_priority_fee", False - ), - - # Trading filters - match_string=cfg["filters"].get("match_string"), - bro_address=cfg["filters"].get("bro_address"), - marry_mode=cfg["filters"].get("marry_mode", False), - yolo_mode=cfg["filters"].get("yolo_mode", False), - ) + # Get and validate platform from configuration + try: + platform = get_platform_from_config(cfg) + logging.info(f"Detected platform: {platform.value}") + except ValueError as e: + logging.exception(f"Platform configuration error: {e}") + return - await trader.start() + # Validate platform support + try: + from platforms import platform_factory + if not platform_factory.registry.is_platform_supported(platform): + logging.error(f"Platform {platform.value} is not supported. Available platforms: {[p.value for p in platform_factory.get_supported_platforms()]}") + return + except Exception as e: + logging.exception(f"Could not validate platform support: {e}") + return + + # Validate listener compatibility + listener_type = cfg["filters"]["listener_type"] + if not validate_platform_listener_combination(platform, listener_type): + from config_loader import get_supported_listeners_for_platform + supported = get_supported_listeners_for_platform(platform) + logging.error(f"Listener '{listener_type}' is not compatible with platform '{platform.value}'. Supported listeners: {supported}") + return + + # Initialize universal trader with platform-specific configuration + try: + trader = UniversalTrader( + # Connection settings + rpc_endpoint=cfg["rpc_endpoint"], + wss_endpoint=cfg["wss_endpoint"], + private_key=cfg["private_key"], + + # Platform configuration - pass platform enum directly + platform=platform, + + # Trade parameters + buy_amount=cfg["trade"]["buy_amount"], + buy_slippage=cfg["trade"]["buy_slippage"], + sell_slippage=cfg["trade"]["sell_slippage"], + + # Extreme fast mode settings + extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False), + extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30), + + # Exit strategy configuration + exit_strategy=cfg["trade"].get("exit_strategy", "time_based"), + take_profit_percentage=cfg["trade"].get("take_profit_percentage"), + stop_loss_percentage=cfg["trade"].get("stop_loss_percentage"), + max_hold_time=cfg["trade"].get("max_hold_time"), + price_check_interval=cfg["trade"].get("price_check_interval", 10), + + # Listener configuration + listener_type=cfg["filters"]["listener_type"], + + # Geyser configuration (if applicable) + geyser_endpoint=cfg.get("geyser", {}).get("endpoint"), + geyser_api_token=cfg.get("geyser", {}).get("api_token"), + geyser_auth_type=cfg.get("geyser", {}).get("auth_type", "x-token"), + + # PumpPortal configuration (if applicable) + pumpportal_url=cfg.get("pumpportal", {}).get( + "url", "wss://pumpportal.fun/api/data" + ), + + # Priority fee configuration + enable_dynamic_priority_fee=cfg.get("priority_fees", {}).get( + "enable_dynamic", False + ), + enable_fixed_priority_fee=cfg.get("priority_fees", {}).get( + "enable_fixed", True + ), + fixed_priority_fee=cfg.get("priority_fees", {}).get("fixed_amount", 500000), + extra_priority_fee=cfg.get("priority_fees", {}).get("extra_percentage", 0.0), + hard_cap_prior_fee=cfg.get("priority_fees", {}).get("hard_cap", 500000), + + # Retry and timeout settings + max_retries=cfg.get("retries", {}).get("max_attempts", 10), + wait_time_after_creation=cfg.get("retries", {}).get("wait_after_creation", 15), + wait_time_after_buy=cfg.get("retries", {}).get("wait_after_buy", 15), + wait_time_before_new_token=cfg.get("retries", {}).get( + "wait_before_new_token", 15 + ), + max_token_age=cfg.get("filters", {}).get("max_token_age", 0.001), + token_wait_timeout=cfg.get("timing", {}).get("token_wait_timeout", 120), + + # Cleanup settings + cleanup_mode=cfg.get("cleanup", {}).get("mode", "disabled"), + cleanup_force_close_with_burn=cfg.get("cleanup", {}).get( + "force_close_with_burn", False + ), + cleanup_with_priority_fee=cfg.get("cleanup", {}).get( + "with_priority_fee", False + ), + + # Trading filters + match_string=cfg["filters"].get("match_string"), + bro_address=cfg["filters"].get("bro_address"), + marry_mode=cfg["filters"].get("marry_mode", False), + yolo_mode=cfg["filters"].get("yolo_mode", False), + ) + + await trader.start() + + except Exception as e: + logging.exception(f"Failed to initialize or start trader: {e}") + raise def run_bot_process(config_path): @@ -131,11 +150,7 @@ def run_bot_process(config_path): def run_all_bots(): - """ - Run all bots defined in YAML files in the 'bots' directory. - Only runs bots that have enabled=True (or where enabled is not specified). - Bots can be run in separate processes based on their configuration. - """ + """Run all bots defined in YAML files in the 'bots' directory.""" bot_dir = Path("bots") if not bot_dir.exists(): logging.error(f"Bot directory '{bot_dir}' not found") @@ -162,19 +177,32 @@ def run_all_bots(): skipped_bots += 1 continue - # Validate platform support + # Validate platform configuration try: platform = get_platform_from_config(cfg) + + # Check platform support from platforms import platform_factory if not platform_factory.registry.is_platform_supported(platform): - logging.error(f"Platform {platform.value} is not supported for bot '{bot_name}'. Skipping...") + logging.error(f"Platform {platform.value} is not supported for bot '{bot_name}'. Available platforms: {[p.value for p in platform_factory.get_supported_platforms()]}") skipped_bots += 1 continue + + # Validate listener compatibility + listener_type = cfg["filters"]["listener_type"] + if not validate_platform_listener_combination(platform, listener_type): + from config_loader import get_supported_listeners_for_platform + supported = get_supported_listeners_for_platform(platform) + logging.error(f"Listener '{listener_type}' is not compatible with platform '{platform.value}' for bot '{bot_name}'. Supported listeners: {supported}") + skipped_bots += 1 + continue + except Exception as e: logging.exception(f"Invalid platform configuration for bot '{bot_name}': {e}. Skipping...") skipped_bots += 1 continue + # Start bot in separate process or main process if cfg.get("separate_process", False): logging.info(f"Starting bot '{bot_name}' ({platform.value}) in separate process") p = multiprocessing.Process( @@ -185,13 +213,16 @@ def run_all_bots(): else: logging.info(f"Starting bot '{bot_name}' ({platform.value}) in main process") asyncio.run(start_bot(str(file))) + except Exception as e: logging.exception(f"Failed to start bot from {file}: {e}") + skipped_bots += 1 logging.info( f"Started {len(bot_files) - skipped_bots} bots, skipped {skipped_bots} disabled/invalid bots" ) + # Wait for all processes to complete for p in processes: p.join() logging.info(f"Process {p.name} completed") @@ -203,11 +234,18 @@ def main() -> None: format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) - # Log supported platforms + # 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}") diff --git a/src/config_loader.py b/src/config_loader.py index 8d0313d..0a58c60 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -1,8 +1,5 @@ """ -Updated configuration validation with platform support. - -This extends the existing config_loader.py to support platform selection -and validation while maintaining backward compatibility. +Updated configuration validation with comprehensive platform support. """ import os @@ -27,77 +24,33 @@ REQUIRED_FIELDS = [ ] CONFIG_VALIDATION_RULES = [ - # (path, type, min_value, max_value, error_message) - ( - "trade.buy_amount", - (int, float), - 0, - float("inf"), - "trade.buy_amount must be a positive number", - ), + ("trade.buy_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 (extended with platform support) +# Valid values for enum-like fields VALID_VALUES = { "filters.listener_type": ["logs", "blocks", "geyser", "pumpportal"], "cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"], "trade.exit_strategy": ["time_based", "tp_sl", "manual"], - "platform": ["pump_fun", "lets_bonk"], # Platform validation + "platform": ["pump_fun", "lets_bonk"], +} + +# Platform-specific listener compatibility +PLATFORM_LISTENER_COMPATIBILITY = { + Platform.PUMP_FUN: ["logs", "blocks", "geyser", "pumpportal"], + Platform.LETS_BONK: ["logs", "blocks", "geyser"], # PumpPortal is pump.fun only } def load_bot_config(path: str) -> dict: - """ - Load and validate a bot configuration from a YAML file. - Extended to support platform selection with backward compatibility. - - Args: - path: Path to the YAML configuration file (relative or absolute) - - Returns: - Validated configuration dictionary - - Raises: - FileNotFoundError: If the configuration file doesn't exist - ValueError: If the configuration is invalid - """ + """Load and validate a bot configuration from a YAML file.""" with open(path) as f: config = yaml.safe_load(f) @@ -107,7 +60,6 @@ def load_bot_config(path: str) -> dict: if os.path.exists(env_path): load_dotenv(env_path, override=True) else: - # If not found relative to config, try relative to current working directory load_dotenv(env_file, override=True) resolve_env_vars(config) @@ -117,18 +69,11 @@ def load_bot_config(path: str) -> dict: config["platform"] = "pump_fun" validate_config(config) - return config def resolve_env_vars(config: dict) -> None: - """ - Recursively resolve environment variables in the configuration. - - Args: - config: Configuration dictionary to process - """ - + """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] @@ -149,19 +94,7 @@ def resolve_env_vars(config: dict) -> None: def get_nested_value(config: dict, path: str) -> Any: - """ - Get a nested value from the configuration using dot notation. - - Args: - config: Configuration dictionary - path: Path to the value using dot notation (e.g., "trade.buy_amount") - - Returns: - The value at the specified path - - Raises: - ValueError: If the path doesn't exist in the configuration - """ + """Get a nested value from the configuration using dot notation.""" keys = path.split(".") value = config for key in keys: @@ -172,16 +105,7 @@ def get_nested_value(config: dict, path: str) -> Any: def validate_config(config: dict) -> None: - """ - Validate the configuration against defined rules. - Extended to include platform-specific validation. - - Args: - config: Configuration dictionary to validate - - Raises: - ValueError: If the configuration is invalid - """ + """Validate the configuration against defined rules with platform support.""" # Validate required fields for field in REQUIRED_FIELDS: get_nested_value(config, field) @@ -198,10 +122,8 @@ def validate_config(config: dict) -> None: raise ValueError(f"Range error: {error_msg}") except ValueError as e: - # Re-raise if it's our own error if str(e).startswith(("Type error:", "Range error:")): raise - # Otherwise, the field might be missing continue # Validate enum-like fields @@ -211,7 +133,6 @@ def validate_config(config: dict) -> None: if value not in valid_values: raise ValueError(f"{path} must be one of {valid_values}") except ValueError: - # Skip if the field is missing continue # Cannot enable both dynamic and fixed priority fees @@ -219,11 +140,8 @@ 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: - # Skip if one of the fields is missing pass # Platform-specific validation @@ -238,16 +156,30 @@ def validate_config(config: dict) -> None: def validate_platform_config(config: dict, platform: Platform) -> None: - """ - Validate platform-specific configuration requirements. + """Validate platform-specific configuration requirements.""" + # 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()]}") + except ImportError: + # If platform factory not available, just validate enum + pass - Args: - config: Configuration dictionary - platform: Platform enum value + # Validate listener compatibility with platform + 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}'. " + f"Compatible listeners: {compatible_listeners}" + ) + except ValueError: + pass - Raises: - ValueError: If platform-specific config is invalid - """ + # Platform-specific configuration validation if platform == Platform.PUMP_FUN: # pump.fun doesn't require additional config beyond base requirements pass @@ -259,18 +191,7 @@ def validate_platform_config(config: dict, platform: Platform) -> None: def get_platform_from_config(config: dict) -> Platform: - """ - Extract platform enum from configuration. - - Args: - config: Configuration dictionary - - Returns: - Platform enum value - - Raises: - ValueError: If platform is invalid - """ + """Extract platform enum from configuration.""" platform_str = config.get("platform", "pump_fun") try: return Platform(platform_str) @@ -278,46 +199,156 @@ def get_platform_from_config(config: dict) -> Platform: raise ValueError(f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}") -def print_config_summary(config: dict) -> None: - """ - Print a summary of the loaded configuration. - Extended to show platform information. - +def validate_platform_listener_combination(platform: Platform, listener_type: str) -> bool: + """Check if a platform and listener type are compatible. + Args: - config: Configuration dictionary + platform: Platform enum + listener_type: Listener type string + + Returns: + True if combination is valid """ + compatible_listeners = PLATFORM_LISTENER_COMPATIBILITY.get(platform, []) + return listener_type in compatible_listeners + + +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 + """ + return PLATFORM_LISTENER_COMPATIBILITY.get(platform, []) + + +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 + """ + if platform == Platform.PUMP_FUN: + return [] # No additional requirements + elif platform == Platform.LETS_BONK: + return [] # No additional requirements yet + else: + return [] + + +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}'") + except ValueError: + print(f"WARNING: Invalid platform '{platform_str}'") trade = config.get("trade", {}) print("Trade settings:") print(f" - Buy amount: {trade.get('buy_amount', 'not configured')} SOL") print(f" - Buy slippage: {trade.get('buy_slippage', 'not configured') * 100}%") - print( - f" - Extreme fast mode: {'enabled' if trade.get('extreme_fast_mode') else 'disabled'}" - ) + 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!") -if __name__ == "__main__": - # Example usage with platform configuration - config = load_bot_config("bots/bot-sniper.yaml") - print_config_summary(config) +def validate_all_platform_configs(config_dir: str = "bots") -> dict[str, Any]: + """Validate all bot configurations in a directory. - platform = get_platform_from_config(config) - print(f"Detected platform: {platform}") \ No newline at end of file + Args: + config_dir: Directory containing bot config files + + Returns: + Dictionary with validation results + """ + import glob + import os + + results = { + "valid_configs": [], + "invalid_configs": [], + "platform_distribution": {}, + "listener_distribution": {}, + } + + config_files = glob.glob(os.path.join(config_dir, "*.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) + }) + + # 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 + + except Exception as 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)}") + except Exception as e: + print(f"Configuration error: {e}") + else: + # Validate all configs in bots directory + results = validate_all_platform_configs() + print("Configuration validation results:") + print(f"Valid configs: {len(results['valid_configs'])}") + 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']: + print("\nInvalid configurations:") + for invalid in results['invalid_configs']: + print(f" {invalid['file']}: {invalid['error']}") \ No newline at end of file diff --git a/src/core/curve.py b/src/core/curve.py new file mode 100644 index 0000000..d4ddf32 --- /dev/null +++ b/src/core/curve.py @@ -0,0 +1,165 @@ +""" +Pump.fun bonding curve manager for price calculations and curve state management. +""" + +import struct +from dataclasses import dataclass + +from solders.pubkey import Pubkey + +from core.client import SolanaClient +from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS +from utils.logger import get_logger + +logger = get_logger(__name__) + +# Bonding curve discriminator +CURVE_DISCRIMINATOR = bytes([23, 203, 71, 8, 209, 70, 227, 3]) + + +@dataclass +class BondingCurveState: + """Represents the state of a pump.fun bonding curve.""" + + virtual_token_reserves: int + virtual_sol_reserves: int + real_token_reserves: int + real_sol_reserves: int + token_total_supply: int + complete: bool + creator: Pubkey + + @property + def token_reserves(self) -> float: + """Token reserves in decimal form.""" + return self.virtual_token_reserves / 10**TOKEN_DECIMALS + + @property + def sol_reserves(self) -> float: + """SOL reserves in decimal form.""" + return self.virtual_sol_reserves / LAMPORTS_PER_SOL + + def calculate_price(self) -> float: + """Calculate current token price in SOL.""" + if self.virtual_token_reserves <= 0: + return 0.0 + + # Price = sol_reserves / token_reserves + price_lamports = self.virtual_sol_reserves / self.virtual_token_reserves + return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL + + +class BondingCurveManager: + """Manages pump.fun bonding curve operations.""" + + def __init__(self, client: SolanaClient): + """Initialize bonding curve manager. + + Args: + client: Solana RPC client + """ + self.client = client + + async def get_curve_state(self, curve_address: Pubkey) -> BondingCurveState: + """Get the current state of a bonding curve. + + Args: + curve_address: Address of the bonding curve + + Returns: + BondingCurveState with current curve data + + Raises: + ValueError: If curve data is invalid or inaccessible + """ + try: + account = await self.client.get_account_info(curve_address) + if not account.data: + raise ValueError(f"No data in bonding curve account {curve_address}") + + curve_state = self._decode_curve_state(account.data) + return curve_state + + except Exception as e: + logger.error(f"Failed to get curve state: {e!s}") + raise ValueError(f"Invalid bonding curve state: {e!s}") + + async def calculate_price(self, curve_address: Pubkey) -> float: + """Calculate current token price from bonding curve. + + Args: + curve_address: Address of the bonding curve + + Returns: + Current token price in SOL + """ + curve_state = await self.get_curve_state(curve_address) + return curve_state.calculate_price() + + def _decode_curve_state(self, data: bytes) -> BondingCurveState: + """Decode bonding curve state from raw account data. + + Args: + data: Raw account data + + Returns: + Decoded BondingCurveState + + Raises: + ValueError: If data format is invalid + """ + if len(data) < 8: + raise ValueError("Curve data too short") + + # Check discriminator + if not data.startswith(CURVE_DISCRIMINATOR): + raise ValueError("Invalid curve discriminator") + + offset = 8 + + try: + # Decode based on pump.fun BondingCurve structure: + # - virtual_token_reserves: u64 (8 bytes) + # - virtual_sol_reserves: u64 (8 bytes) + # - real_token_reserves: u64 (8 bytes) + # - real_sol_reserves: u64 (8 bytes) + # - token_total_supply: u64 (8 bytes) + # - complete: bool (1 byte) + # - padding: 7 bytes + # - creator: Pubkey (32 bytes) + + virtual_token_reserves = struct.unpack_from(" None: - """Listen for new token creations using Geyser subscription. - - Args: - token_callback: Callback function for new tokens - match_string: Optional string to match in token name/symbol - creator_address: Optional creator address to filter by - """ + """Listen for new token creations using Geyser subscription.""" if not self.platform_parsers: logger.error("No platform parsers available. Cannot listen for tokens.") return @@ -161,8 +148,12 @@ class UniversalGeyserListener(BaseTokenListener): await token_callback(token_info) - except grpc.aio.AioRpcError as e: - logger.error(f"gRPC error: {e.details()}") + except Exception as e: + import grpc + if isinstance(e, grpc.aio.AioRpcError): + logger.error(f"gRPC error: {e.details()}") + else: + logger.error(f"Geyser error: {e}") await asyncio.sleep(5) finally: @@ -174,14 +165,7 @@ class UniversalGeyserListener(BaseTokenListener): await asyncio.sleep(10) async def _process_update(self, update) -> TokenInfo | None: - """Process a Geyser update and extract token creation info. - - Args: - update: Geyser update from the subscription - - Returns: - TokenInfo if a token creation is found, None otherwise - """ + """Process a Geyser update and extract token creation info.""" try: if not update.HasField("transaction"): return None @@ -191,6 +175,8 @@ class UniversalGeyserListener(BaseTokenListener): if msg is None: return None + from solders.pubkey import Pubkey + for ix in msg.instructions: # Check which platform this instruction belongs to program_idx = ix.program_id_index diff --git a/src/monitoring/universal_logs_listener.py b/src/monitoring/universal_logs_listener.py index 56c5f03..5ebb32f 100644 --- a/src/monitoring/universal_logs_listener.py +++ b/src/monitoring/universal_logs_listener.py @@ -1,7 +1,6 @@ """ Universal logs listener that works with any platform through the interface system. """ - import asyncio import json from collections.abc import Awaitable, Callable @@ -10,7 +9,6 @@ import websockets from interfaces.core import Platform, TokenInfo from monitoring.base_listener import BaseTokenListener -from platforms import get_platform_implementations from utils.logger import get_logger logger = get_logger(__name__) @@ -47,13 +45,13 @@ class UniversalLogsListener(BaseTokenListener): self.platform_parsers = {} self.platform_program_ids = [] + # Create a temporary client for getting parsers (stateless parsers don't use it) + from core.client import SolanaClient + temp_client = SolanaClient("http://temp") + for platform in self.platforms: try: - # We'll need a dummy client for getting the parser - from core.client import SolanaClient - dummy_client = SolanaClient("http://localhost") # Won't be used for parsing - - implementations = get_platform_implementations(platform, dummy_client) + implementations = platform_factory.create_for_platform(platform, temp_client) parser = implementations.event_parser self.platform_parsers[platform] = parser self.platform_program_ids.append(str(parser.get_program_id())) @@ -130,11 +128,11 @@ class UniversalLogsListener(BaseTokenListener): websocket: Active WebSocket connection """ # Subscribe to logs for all monitored platforms - for program_id in self.platform_program_ids: + for i, program_id in enumerate(self.platform_program_ids): subscription_message = json.dumps( { "jsonrpc": "2.0", - "id": len(self.platform_program_ids), # Use different IDs + "id": i + 1, "method": "logsSubscribe", "params": [ {"mentions": [program_id]}, @@ -155,11 +153,7 @@ class UniversalLogsListener(BaseTokenListener): logger.warning(f"Unexpected subscription response: {response}") async def _ping_loop(self, websocket) -> None: - """Keep connection alive with pings. - - Args: - websocket: Active WebSocket connection - """ + """Keep connection alive with pings.""" try: while True: await asyncio.sleep(self.ping_interval) @@ -168,7 +162,6 @@ class UniversalLogsListener(BaseTokenListener): await asyncio.wait_for(pong_waiter, timeout=10) except TimeoutError: logger.warning("Ping timeout - server not responding") - # Force reconnection await websocket.close() return except asyncio.CancelledError: @@ -177,14 +170,7 @@ class UniversalLogsListener(BaseTokenListener): logger.error(f"Ping error: {e!s}") async def _wait_for_token_creation(self, websocket) -> TokenInfo | None: - """Wait for token creation events from any platform. - - Args: - websocket: Active WebSocket connection - - Returns: - TokenInfo if a token creation is found, None otherwise - """ + """Wait for token creation events from any platform.""" try: response = await asyncio.wait_for(websocket.recv(), timeout=30) data = json.loads(response) diff --git a/src/platforms/__init__.py b/src/platforms/__init__.py index 9348112..7c0db22 100644 --- a/src/platforms/__init__.py +++ b/src/platforms/__init__.py @@ -32,7 +32,7 @@ class PlatformRegistry: def __init__(self): self._implementations: dict[Platform, dict[str, type]] = {} - self._instances: dict[Platform, PlatformImplementations] = {} + self._instances: dict[tuple[Platform, str], PlatformImplementations] = {} def register_platform( self, @@ -80,13 +80,16 @@ class PlatformRegistry: if platform not in self._implementations: raise ValueError(f"Platform {platform} is not registered") - # Check if we already have instances for this platform - if platform in self._instances: - return self._instances[platform] + # 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] - # Create instances + # Create instances - only curve_manager needs client address_provider = impl_classes['address_provider']() instruction_builder = impl_classes['instruction_builder']() curve_manager = impl_classes['curve_manager'](client) @@ -100,20 +103,22 @@ class PlatformRegistry: ) # Cache the instances - self._instances[platform] = implementations + self._instances[cache_key] = implementations return implementations - def get_platform_implementations(self, platform: Platform) -> 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 """ - return self._instances.get(platform) + cache_key = (platform, client_endpoint) + return self._instances.get(cache_key) def get_supported_platforms(self) -> list[Platform]: """Get list of supported platforms. @@ -144,7 +149,7 @@ class PlatformFactory: def _setup_default_platforms(self) -> None: """Setup default platform registrations.""" - # Import platform implementations dynamically to avoid circular imports + # Import and register pump.fun platform try: from platforms.pumpfun import ( PumpFunAddressProvider, @@ -164,6 +169,7 @@ class PlatformFactory: except ImportError as e: print(f"Warning: Could not register pump.fun platform: {e}") + # Import and register LetsBonk platform try: from platforms.letsbonk import ( LetsBonkAddressProvider, diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py index aa18242..8480647 100644 --- a/src/platforms/letsbonk/event_parser.py +++ b/src/platforms/letsbonk/event_parser.py @@ -5,11 +5,13 @@ This module parses LetsBonk-specific token creation events from various sources by implementing the EventParser interface. """ +import base64 import struct from time import monotonic from typing import Any, Final from solders.pubkey import Pubkey +from solders.transaction import VersionedTransaction from interfaces.core import EventParser, Platform, TokenInfo from platforms.letsbonk.address_provider import LetsBonkAddressProvider @@ -184,6 +186,108 @@ class LetsBonkEventParser(EventParser): """ return [self.INITIALIZE_DISCRIMINATOR] + 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 + """ + try: + if "transactions" not in block_data: + return None + + for tx in block_data["transactions"]: + if not isinstance(tx, dict) or "transaction" not in tx: + continue + + # Decode base64 transaction data if needed + tx_data = tx["transaction"] + if isinstance(tx_data, list) and len(tx_data) > 0: + try: + 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] + + # 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(" dict | None: """Parse the initialize instruction data from LetsBonk. @@ -240,33 +344,4 @@ class LetsBonkEventParser(EventParser): return parsed_data except Exception: - return None - - def parse_token_creation_from_block(self, block_data: dict) -> list[TokenInfo]: - """Parse token creations from block data (for block listener). - - Args: - block_data: Block data from WebSocket - - Returns: - List of TokenInfo for any token creations found - """ - tokens = [] - - try: - if "transactions" not in block_data: - return tokens - - for tx in block_data["transactions"]: - if not isinstance(tx, dict) or "transaction" not in tx: - continue - - # Process transaction (implementation would be similar to pump.fun) - # This is a simplified version - full implementation would decode - # the transaction and check for LetsBonk initialize instructions - pass - - return tokens - - except Exception: - return tokens \ No newline at end of file + return None \ No newline at end of file diff --git a/src/trading/platform_aware.py b/src/trading/platform_aware.py index 7770991..ba95bbc 100644 --- a/src/trading/platform_aware.py +++ b/src/trading/platform_aware.py @@ -1,8 +1,6 @@ """ Platform-aware trader implementations that use the interface system. - -This module provides new trader classes that work with any platform -through the interface system, while maintaining compatibility with existing code. +Final cleanup removing all platform-specific hardcoding. """ from solders.pubkey import Pubkey @@ -33,18 +31,7 @@ class PlatformAwareBuyer(Trader): extreme_fast_token_amount: int = 0, extreme_fast_mode: bool = False, ): - """Initialize platform-aware token buyer. - - Args: - client: Solana client for RPC calls - wallet: Wallet for signing transactions - priority_fee_manager: Priority fee manager - amount: Amount of SOL to spend - slippage: Slippage tolerance (0.01 = 1%) - max_retries: Maximum number of retry attempts - extreme_fast_token_amount: Amount of token to buy if extreme fast mode is enabled - extreme_fast_mode: If enabled, avoid fetching pool state for price estimation - """ + """Initialize platform-aware token buyer.""" self.client = client self.wallet = wallet self.priority_fee_manager = priority_fee_manager @@ -55,14 +42,7 @@ class PlatformAwareBuyer(Trader): self.extreme_fast_token_amount = extreme_fast_token_amount async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult: - """Execute buy operation using platform-specific implementations. - - Args: - token_info: Enhanced token information with platform - - Returns: - TradeResult with buy outcome - """ + """Execute buy operation using platform-specific implementations.""" try: # Get platform-specific implementations implementations = get_platform_implementations(token_info.platform, self.client) @@ -78,7 +58,7 @@ class PlatformAwareBuyer(Trader): token_amount = self.extreme_fast_token_amount token_price_sol = self.amount / token_amount if token_amount > 0 else 0 else: - # Get pool address based on platform + # 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 @@ -92,7 +72,7 @@ class PlatformAwareBuyer(Trader): # Calculate maximum SOL to spend with slippage max_amount_lamports = int(amount_lamports * (1 + self.slippage)) - # Build buy instructions + # Build buy instructions using platform-specific builder instructions = await instruction_builder.build_buy_instruction( token_info, self.wallet.pubkey, @@ -107,7 +87,7 @@ class PlatformAwareBuyer(Trader): ) logger.info( - f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token" + f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token on {token_info.platform.value}" ) logger.info( f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)" @@ -151,22 +131,17 @@ class PlatformAwareBuyer(Trader): ) def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey: - """Get the pool/curve address for price calculations. - - Args: - token_info: Token information - address_provider: Platform address provider - - Returns: - Pool/curve address - """ + """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: - return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint) + if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve: + return token_info.bonding_curve elif token_info.platform == Platform.LETS_BONK: - return token_info.pool_state or address_provider.derive_pool_address(token_info.mint) - else: - # Fallback to deriving the address - return address_provider.derive_pool_address(token_info.mint) + 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) class PlatformAwareSeller(Trader): @@ -180,15 +155,7 @@ class PlatformAwareSeller(Trader): slippage: float = 0.25, max_retries: int = 5, ): - """Initialize platform-aware token seller. - - Args: - client: Solana client for RPC calls - wallet: Wallet for signing transactions - priority_fee_manager: Priority fee manager - slippage: Slippage tolerance (0.25 = 25%) - max_retries: Maximum number of retry attempts - """ + """Initialize platform-aware token seller.""" self.client = client self.wallet = wallet self.priority_fee_manager = priority_fee_manager @@ -196,14 +163,7 @@ class PlatformAwareSeller(Trader): self.max_retries = max_retries async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult: - """Execute sell operation using platform-specific implementations. - - Args: - token_info: Enhanced token information with platform - - Returns: - TradeResult with sell outcome - """ + """Execute sell operation using platform-specific implementations.""" try: # Get platform-specific implementations implementations = get_platform_implementations(token_info.platform, self.client) @@ -229,7 +189,7 @@ class PlatformAwareSeller(Trader): error_message="No tokens to sell" ) - # Get pool address and current price + # Get pool address and current price using platform-agnostic method pool_address = self._get_pool_address(token_info, address_provider) token_price_sol = await curve_manager.calculate_price(pool_address) @@ -239,13 +199,13 @@ class PlatformAwareSeller(Trader): expected_sol_output = float(token_balance_decimal) * float(token_price_sol) min_sol_output = int((expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL) - logger.info(f"Selling {token_balance_decimal} tokens") + 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" ) - # Build sell instructions + # Build sell instructions using platform-specific builder instructions = await instruction_builder.build_sell_instruction( token_info, self.wallet.pubkey, @@ -297,19 +257,14 @@ class PlatformAwareSeller(Trader): ) def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey: - """Get the pool/curve address for price calculations. - - Args: - token_info: Token information - address_provider: Platform address provider - - Returns: - Pool/curve address - """ + """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: - return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint) + if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve: + return token_info.bonding_curve elif token_info.platform == Platform.LETS_BONK: - return token_info.pool_state or address_provider.derive_pool_address(token_info.mint) - else: - # Fallback to deriving the address - return address_provider.derive_pool_address(token_info.mint) \ No newline at end of file + 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 diff --git a/src/trading/universal_trader.py b/src/trading/universal_trader.py index 2e10ae4..40c4004 100644 --- a/src/trading/universal_trader.py +++ b/src/trading/universal_trader.py @@ -1,6 +1,6 @@ """ Universal trading coordinator that works with any platform. -Replaces PumpTrader with platform-agnostic implementation. +Cleaned up to remove all platform-specific hardcoding. """ import asyncio @@ -84,18 +84,7 @@ class UniversalTrader: marry_mode: bool = False, yolo_mode: bool = False, ): - """Initialize the universal trader. - - Args: - rpc_endpoint: RPC endpoint URL - wss_endpoint: WebSocket endpoint URL - private_key: Wallet private key - buy_amount: Amount of SOL to spend on buys - buy_slippage: Slippage tolerance for buys - sell_slippage: Slippage tolerance for sells - platform: Platform to trade on (Platform enum or string) - ... (other args same as PumpTrader) - """ + """Initialize the universal trader.""" # Core components self.solana_client = SolanaClient(rpc_endpoint) self.wallet = Wallet(private_key) @@ -116,6 +105,15 @@ class UniversalTrader: logger.info(f"Initialized Universal Trader for platform: {self.platform.value}") + # 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 as e: + logger.error(f"Platform validation failed: {e}") + raise + # Get platform-specific implementations self.platform_implementations = get_platform_implementations( self.platform, self.solana_client @@ -141,7 +139,7 @@ class UniversalTrader: max_retries, ) - # Initialize the appropriate listener + # Initialize the appropriate listener with platform filtering self.token_listener = ListenerFactory.create_listener( listener_type=listener_type, wss_endpoint=wss_endpoint, @@ -253,11 +251,7 @@ class UniversalTrader: logger.info("Universal Trader has shut down") async def _wait_for_token(self) -> TokenInfo | None: - """Wait for a single token to be detected. - - Returns: - TokenInfo or None if timeout occurs - """ + """Wait for a single token to be detected.""" # Create a one-time event to signal when a token is found token_found = asyncio.Event() found_token = None @@ -322,11 +316,7 @@ class UniversalTrader: await self.solana_client.close() async def _queue_token(self, token_info: TokenInfo) -> None: - """Queue a token for processing if not already processed. - - Args: - token_info: Token information to queue - """ + """Queue a token for processing if not already processed.""" token_key = str(token_info.mint) if token_key in self.processed_tokens: @@ -360,7 +350,6 @@ class UniversalTrader: await self._handle_token(token_info) except asyncio.CancelledError: - # Handle cancellation gracefully logger.info("Token queue processor was cancelled") break except Exception as e: @@ -369,11 +358,7 @@ class UniversalTrader: self.token_queue.task_done() async def _handle_token(self, token_info: TokenInfo) -> None: - """Handle a new token creation event. - - Args: - token_info: Token information - """ + """Handle a new token creation event.""" try: # Validate that token is for our platform if token_info.platform != self.platform: @@ -404,12 +389,7 @@ class UniversalTrader: logger.error(f"Error handling token {token_info.symbol}: {e!s}") async def _handle_successful_buy(self, token_info: TokenInfo, buy_result: TradeResult) -> None: - """Handle successful token purchase. - - Args: - token_info: Token information - buy_result: The result of the buy operation - """ + """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) self.traded_mints.add(token_info.mint) @@ -426,12 +406,7 @@ class UniversalTrader: logger.info("Marry mode enabled. Skipping sell operation.") async def _handle_failed_buy(self, token_info: TokenInfo, buy_result: TradeResult) -> None: - """Handle failed token purchase. - - Args: - token_info: Token information - buy_result: The result of the buy operation - """ + """Handle failed token purchase.""" logger.error(f"Failed to buy {token_info.symbol}: {buy_result.error_message}") # Close ATA if enabled await handle_cleanup_after_failure( @@ -445,12 +420,7 @@ class UniversalTrader: ) async def _handle_tp_sl_exit(self, token_info: TokenInfo, buy_result: TradeResult) -> None: - """Handle take profit/stop loss exit strategy. - - Args: - token_info: Token information - buy_result: Result from the buy operation - """ + """Handle take profit/stop loss exit strategy.""" # Create position position = Position.create_from_buy_result( mint=token_info.mint, @@ -472,11 +442,7 @@ class UniversalTrader: await self._monitor_position_until_exit(token_info, position) async def _handle_time_based_exit(self, token_info: TokenInfo) -> None: - """Handle legacy time-based exit strategy. - - Args: - token_info: Token information - """ + """Handle legacy time-based exit strategy.""" logger.info(f"Waiting for {self.wait_time_after_buy} seconds before selling...") await asyncio.sleep(self.wait_time_after_buy) @@ -500,15 +466,10 @@ class UniversalTrader: 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: - """Monitor a position until exit conditions are met. - - Args: - token_info: Token information - position: Position to monitor - """ + """Monitor a position until exit conditions are met.""" logger.info(f"Starting position monitoring (check interval: {self.price_check_interval}s)") - # Get pool address for price monitoring + # Get pool address for price monitoring using platform-agnostic method pool_address = self._get_pool_address(token_info) curve_manager = self.platform_implementations.curve_manager @@ -570,35 +531,25 @@ class UniversalTrader: 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. - - Args: - token_info: Token information - - Returns: - Pool/curve address - """ + """Get the pool/curve address for price monitoring using platform-agnostic method.""" address_provider = self.platform_implementations.address_provider - if token_info.platform == Platform.PUMP_FUN: - return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint) - elif token_info.platform == Platform.LETS_BONK: - return token_info.pool_state or address_provider.derive_pool_address(token_info.mint) + # Use platform-specific logic to get the appropriate address + 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: + return token_info.pool_state else: - # Fallback to deriving the address + # Fallback to deriving the address using platform provider return address_provider.derive_pool_address(token_info.mint) async def _save_token_info(self, token_info: TokenInfo) -> None: - """Save token information to a file. - - Args: - token_info: Token information - """ + """Save token information to a file.""" try: os.makedirs("trades", exist_ok=True) file_name = os.path.join("trades", f"{token_info.mint}.txt") - # Convert to dictionary for saving + # Convert to dictionary for saving - platform-agnostic token_dict = { "name": token_info.name, "symbol": token_info.symbol, @@ -607,14 +558,22 @@ class UniversalTrader: "platform": token_info.platform.value, "user": str(token_info.user) if token_info.user else None, "creator": str(token_info.creator) if token_info.creator else None, - # Platform-specific fields - "bonding_curve": str(token_info.bonding_curve) if token_info.bonding_curve else None, - "associated_bonding_curve": str(token_info.associated_bonding_curve) if token_info.associated_bonding_curve else None, - "creator_vault": str(token_info.creator_vault) if token_info.creator_vault else None, - "pool_state": str(token_info.pool_state) if token_info.pool_state else None, - "base_vault": str(token_info.base_vault) if token_info.base_vault else None, - "quote_vault": str(token_info.quote_vault) if token_info.quote_vault else None, + "creation_timestamp": token_info.creation_timestamp, } + + # Add platform-specific fields only if they exist + platform_fields = { + "bonding_curve": token_info.bonding_curve, + "associated_bonding_curve": token_info.associated_bonding_curve, + "creator_vault": token_info.creator_vault, + "pool_state": token_info.pool_state, + "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) with open(file_name, "w") as file: file.write(json.dumps(token_dict, indent=2)) @@ -624,15 +583,7 @@ class UniversalTrader: logger.error(f"Failed to save token information: {e!s}") def _log_trade(self, action: str, token_info: TokenInfo, price: float, amount: float, tx_hash: str | None) -> None: - """Log trade information. - - Args: - action: Trade action (buy/sell) - token_info: Token information - price: Token price in SOL - amount: Trade amount in SOL - tx_hash: Transaction hash - """ + """Log trade information.""" try: os.makedirs("trades", exist_ok=True) From 8ee6b95d2d7879b1021976c6ad7fe9d8bc86afd6 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 14:03:41 +0000 Subject: [PATCH 04/30] feat(core): cleanup address management system --- src/core/pubkeys.py | 77 ++++-------- src/monitoring/pumpportal_event_processor.py | 16 +-- src/monitoring/pumpportal_listener.py | 14 ++- src/platforms/letsbonk/address_provider.py | 91 +++++++------- src/platforms/letsbonk/instruction_builder.py | 18 +-- src/platforms/pumpfun/address_provider.py | 112 +++++++++++++----- src/platforms/pumpfun/event_parser.py | 7 +- 7 files changed, 183 insertions(+), 152 deletions(-) diff --git a/src/core/pubkeys.py b/src/core/pubkeys.py index 6bc28f8..e365ee5 100644 --- a/src/core/pubkeys.py +++ b/src/core/pubkeys.py @@ -1,5 +1,7 @@ """ -System and program addresses for Solana and pump.fun interactions. +System addresses and constants for Solana blockchain operations. +This module contains only system-level addresses that are shared across all platforms. +Platform-specific addresses are handled by their respective AddressProvider implementations. """ from dataclasses import dataclass @@ -7,76 +9,45 @@ from typing import Final from solders.pubkey import Pubkey +# Constants LAMPORTS_PER_SOL: Final[int] = 1_000_000_000 TOKEN_DECIMALS: Final[int] = 6 @dataclass class SystemAddresses: - """System-level Solana addresses.""" + """System-level Solana addresses shared across all platforms.""" - PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111") + # Core system programs + SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111") TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" ) ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" ) + + # System accounts RENT: Final[Pubkey] = Pubkey.from_string( "SysvarRent111111111111111111111111111111111" ) - SOL: Final[Pubkey] = Pubkey.from_string( + + # Native SOL token + SOL_MINT: Final[Pubkey] = Pubkey.from_string( "So11111111111111111111111111111111111111112" ) - - -@dataclass -class PumpAddresses: - """Pump.fun program addresses.""" - - PROGRAM: Final[Pubkey] = Pubkey.from_string( - "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" - ) - GLOBAL: Final[Pubkey] = Pubkey.from_string( - "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf" - ) - EVENT_AUTHORITY: Final[Pubkey] = Pubkey.from_string( - "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1" - ) - FEE: Final[Pubkey] = Pubkey.from_string( - "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM" - ) - LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string( - "39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg" - ) - - @staticmethod - def find_global_volume_accumulator() -> Pubkey: - """ - Derive the Program Derived Address (PDA) for the global volume accumulator. + + @classmethod + def get_all_system_addresses(cls) -> dict[str, Pubkey]: + """Get all system addresses as a dictionary. Returns: - Pubkey of the derived global volume accumulator account + Dictionary mapping address names to Pubkey objects """ - derived_address, _ = Pubkey.find_program_address( - [b"global_volume_accumulator"], - PumpAddresses.PROGRAM, - ) - return derived_address - - @staticmethod - 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 - """ - derived_address, _ = Pubkey.find_program_address( - [b"user_volume_accumulator", bytes(user)], - PumpAddresses.PROGRAM, - ) - return derived_address + return { + "system_program": cls.SYSTEM_PROGRAM, + "token_program": cls.TOKEN_PROGRAM, + "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/monitoring/pumpportal_event_processor.py b/src/monitoring/pumpportal_event_processor.py index 02b77a4..5033e00 100644 --- a/src/monitoring/pumpportal_event_processor.py +++ b/src/monitoring/pumpportal_event_processor.py @@ -4,8 +4,9 @@ Event processing for pump.fun tokens using PumpPortal data. from solders.pubkey import Pubkey -from core.pubkeys import PumpAddresses, SystemAddresses -from trading.base import TokenInfo +from core.pubkeys import SystemAddresses +from interfaces.core import Platform, TokenInfo +from platforms.pumpfun.address_provider import PumpFunAddresses from utils.logger import get_logger logger = get_logger(__name__) @@ -14,13 +15,13 @@ logger = get_logger(__name__) class PumpPortalEventProcessor: """Processes token creation events from PumpPortal WebSocket.""" - def __init__(self, pump_program: Pubkey): + def __init__(self, pump_program: Pubkey | None = None): """Initialize event processor. Args: - pump_program: Pump.fun program address + pump_program: Pump.fun program address (optional, will use default if None) """ - self.pump_program = pump_program + self.pump_program = pump_program or PumpFunAddresses.PROGRAM def process_token_data(self, token_data: dict) -> TokenInfo | None: """Process token data from PumpPortal and extract token creation info. @@ -71,6 +72,7 @@ class PumpPortalEventProcessor: symbol=symbol, uri=uri, mint=mint, + platform=Platform.PUMP_FUN, bonding_curve=bonding_curve, associated_bonding_curve=associated_bonding_curve, user=user, @@ -118,6 +120,6 @@ class PumpPortalEventProcessor: """ derived_address, _ = Pubkey.find_program_address( [b"creator-vault", bytes(creator)], - PumpAddresses.PROGRAM, + PumpFunAddresses.PROGRAM, ) - return derived_address + return derived_address \ No newline at end of file diff --git a/src/monitoring/pumpportal_listener.py b/src/monitoring/pumpportal_listener.py index 1116a8a..6f8ab63 100644 --- a/src/monitoring/pumpportal_listener.py +++ b/src/monitoring/pumpportal_listener.py @@ -9,9 +9,10 @@ from collections.abc import Awaitable, Callable import websockets from solders.pubkey import Pubkey +from interfaces.core import TokenInfo from monitoring.base_listener import BaseTokenListener from monitoring.pumpportal_event_processor import PumpPortalEventProcessor -from trading.base import TokenInfo +from platforms.pumpfun.address_provider import PumpFunAddresses from utils.logger import get_logger logger = get_logger(__name__) @@ -22,18 +23,19 @@ class PumpPortalListener(BaseTokenListener): def __init__( self, - pump_program: Pubkey, + pump_program: Pubkey | None = None, pumpportal_url: str = "wss://pumpportal.fun/api/data", ): """Initialize token listener. Args: - pump_program: Pump.fun program address + pump_program: Pump.fun program address (optional, will use default if None) pumpportal_url: PumpPortal WebSocket URL """ - self.pump_program = pump_program + super().__init__() + self.pump_program = pump_program or PumpFunAddresses.PROGRAM self.pumpportal_url = pumpportal_url - self.event_processor = PumpPortalEventProcessor(pump_program) + self.event_processor = PumpPortalEventProcessor(self.pump_program) self.ping_interval = 20 # seconds async def listen_for_tokens( @@ -171,4 +173,4 @@ class PumpPortalListener(BaseTokenListener): except Exception as e: logger.error(f"Error processing PumpPortal WebSocket message: {e}") - return None + return None \ No newline at end of file diff --git a/src/platforms/letsbonk/address_provider.py b/src/platforms/letsbonk/address_provider.py index 4877c13..e9ee899 100644 --- a/src/platforms/letsbonk/address_provider.py +++ b/src/platforms/letsbonk/address_provider.py @@ -5,28 +5,29 @@ This module provides all LetsBonk (Raydium LaunchLab) specific addresses and PDA by implementing the AddressProvider interface. """ +from dataclasses import dataclass +from typing import Final from solders.pubkey import Pubkey from spl.token.instructions import get_associated_token_address +from core.pubkeys import SystemAddresses 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") + + class LetsBonkAddressProvider(AddressProvider): """LetsBonk (Raydium LaunchLab) implementation of AddressProvider interface.""" - # Raydium LaunchLab program addresses - RAYDIUM_LAUNCHLAB_PROGRAM_ID = Pubkey.from_string("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj") - GLOBAL_CONFIG = Pubkey.from_string("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX") - LETSBONK_PLATFORM_CONFIG = Pubkey.from_string("FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1") - - # System program addresses - TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") - SYSTEM_PROGRAM_ID = Pubkey.from_string("11111111111111111111111111111111") - WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112") - ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL") - SYSTEM_RENT_PROGRAM_ID = Pubkey.from_string("SysvarRent111111111111111111111111111111111") - @property def platform(self) -> Platform: """Get the platform this provider serves.""" @@ -35,7 +36,7 @@ class LetsBonkAddressProvider(AddressProvider): @property def program_id(self) -> Pubkey: """Get the main program ID for this platform.""" - return self.RAYDIUM_LAUNCHLAB_PROGRAM_ID + return LetsBonkAddresses.PROGRAM def get_system_addresses(self) -> dict[str, Pubkey]: """Get all system addresses required for LetsBonk. @@ -43,19 +44,19 @@ class LetsBonkAddressProvider(AddressProvider): Returns: Dictionary mapping address names to Pubkey objects """ - return { + # 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 - "program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID, - "global_config": self.GLOBAL_CONFIG, - "platform_config": self.LETSBONK_PLATFORM_CONFIG, - - # System addresses - "system_program": self.SYSTEM_PROGRAM_ID, - "token_program": self.TOKEN_PROGRAM_ID, - "associated_token_program": self.ASSOCIATED_TOKEN_PROGRAM_ID, - "rent": self.SYSTEM_RENT_PROGRAM_ID, - "wsol_mint": self.WSOL_MINT, + "program": LetsBonkAddresses.PROGRAM, + "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: """Derive the pool state address for a token pair. @@ -70,11 +71,11 @@ class LetsBonkAddressProvider(AddressProvider): Pool state address """ if quote_mint is None: - quote_mint = self.WSOL_MINT + quote_mint = SystemAddresses.SOL_MINT pool_state, _ = Pubkey.find_program_address( [b"pool", bytes(base_mint), bytes(quote_mint)], - self.RAYDIUM_LAUNCHLAB_PROGRAM_ID + LetsBonkAddresses.PROGRAM ) return pool_state @@ -134,7 +135,7 @@ class LetsBonkAddressProvider(AddressProvider): AUTH_SEED = b"vault_auth_seed" authority_pda, _ = Pubkey.find_program_address( [AUTH_SEED], - self.RAYDIUM_LAUNCHLAB_PROGRAM_ID + LetsBonkAddresses.PROGRAM ) return authority_pda @@ -149,7 +150,7 @@ class LetsBonkAddressProvider(AddressProvider): EVENT_AUTHORITY_SEED = b"__event_authority" event_authority_pda, _ = Pubkey.find_program_address( [EVENT_AUTHORITY_SEED], - self.RAYDIUM_LAUNCHLAB_PROGRAM_ID + LetsBonkAddresses.PROGRAM ) return event_authority_pda @@ -163,7 +164,7 @@ class LetsBonkAddressProvider(AddressProvider): Returns: New WSOL account address """ - return Pubkey.create_with_seed(payer, seed, self.TOKEN_PROGRAM_ID) + return Pubkey.create_with_seed(payer, seed, SystemAddresses.TOKEN_PROGRAM) def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]: """Get all accounts needed for a buy instruction. @@ -180,18 +181,18 @@ class LetsBonkAddressProvider(AddressProvider): return { "payer": user, "authority": additional_accounts["authority"], - "global_config": self.GLOBAL_CONFIG, - "platform_config": self.LETSBONK_PLATFORM_CONFIG, + "global_config": LetsBonkAddresses.GLOBAL_CONFIG, + "platform_config": LetsBonkAddresses.PLATFORM_CONFIG, "pool_state": additional_accounts["pool_state"], "user_base_token": self.derive_user_token_account(user, token_info.mint), "base_vault": additional_accounts.get("base_vault", token_info.base_vault), "quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault), "base_token_mint": token_info.mint, - "quote_token_mint": self.WSOL_MINT, - "base_token_program": self.TOKEN_PROGRAM_ID, - "quote_token_program": self.TOKEN_PROGRAM_ID, + "quote_token_mint": SystemAddresses.SOL_MINT, + "base_token_program": SystemAddresses.TOKEN_PROGRAM, + "quote_token_program": SystemAddresses.TOKEN_PROGRAM, "event_authority": additional_accounts["event_authority"], - "program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID, + "program": LetsBonkAddresses.PROGRAM, } def get_sell_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]: @@ -209,18 +210,18 @@ class LetsBonkAddressProvider(AddressProvider): return { "payer": user, "authority": additional_accounts["authority"], - "global_config": self.GLOBAL_CONFIG, - "platform_config": self.LETSBONK_PLATFORM_CONFIG, + "global_config": LetsBonkAddresses.GLOBAL_CONFIG, + "platform_config": LetsBonkAddresses.PLATFORM_CONFIG, "pool_state": additional_accounts["pool_state"], "user_base_token": self.derive_user_token_account(user, token_info.mint), "base_vault": additional_accounts.get("base_vault", token_info.base_vault), "quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault), "base_token_mint": token_info.mint, - "quote_token_mint": self.WSOL_MINT, - "base_token_program": self.TOKEN_PROGRAM_ID, - "quote_token_program": self.TOKEN_PROGRAM_ID, + "quote_token_mint": SystemAddresses.SOL_MINT, + "base_token_program": SystemAddresses.TOKEN_PROGRAM, + "quote_token_program": SystemAddresses.TOKEN_PROGRAM, "event_authority": additional_accounts["event_authority"], - "program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID, + "program": LetsBonkAddresses.PROGRAM, } def get_wsol_account_creation_accounts(self, user: Pubkey, wsol_account: Pubkey) -> dict[str, Pubkey]: @@ -236,9 +237,9 @@ class LetsBonkAddressProvider(AddressProvider): return { "payer": user, "wsol_account": wsol_account, - "wsol_mint": self.WSOL_MINT, + "wsol_mint": SystemAddresses.SOL_MINT, "owner": user, - "system_program": self.SYSTEM_PROGRAM_ID, - "token_program": self.TOKEN_PROGRAM_ID, - "rent": self.SYSTEM_RENT_PROGRAM_ID, + "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/instruction_builder.py b/src/platforms/letsbonk/instruction_builder.py index b4ff731..106ebf2 100644 --- a/src/platforms/letsbonk/instruction_builder.py +++ b/src/platforms/letsbonk/instruction_builder.py @@ -15,7 +15,7 @@ from solders.pubkey import Pubkey from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed from spl.token.instructions import create_idempotent_associated_token_account -from core.pubkeys import TOKEN_DECIMALS +from core.pubkeys import TOKEN_DECIMALS, SystemAddresses from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo # Instruction discriminators for LetsBonk (from IDL) @@ -63,7 +63,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): user, # payer user, # owner token_info.mint, # mint - address_provider.TOKEN_PROGRAM_ID, # token program + SystemAddresses.TOKEN_PROGRAM, # token program ) instructions.append(ata_instruction) @@ -83,7 +83,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): seed=wsol_seed, lamports=total_lamports, space=165, # Size of a token account - owner=address_provider.TOKEN_PROGRAM_ID + owner=SystemAddresses.TOKEN_PROGRAM ) ) instructions.append(create_wsol_ix) @@ -91,7 +91,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): # 3. Initialize WSOL account initialize_wsol_ix = self._create_initialize_account_instruction( wsol_account, - address_provider.WSOL_MINT, + SystemAddresses.SOL_MINT, user, address_provider ) @@ -183,7 +183,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): seed=wsol_seed, lamports=account_creation_lamports, space=165, # Size of a token account - owner=address_provider.TOKEN_PROGRAM_ID + owner=SystemAddresses.TOKEN_PROGRAM ) ) instructions.append(create_wsol_ix) @@ -191,7 +191,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): # 2. Initialize WSOL account initialize_wsol_ix = self._create_initialize_account_instruction( wsol_account, - address_provider.WSOL_MINT, + SystemAddresses.SOL_MINT, user, address_provider ) @@ -334,14 +334,14 @@ 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=address_provider.SYSTEM_RENT_PROGRAM_ID, 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=address_provider.TOKEN_PROGRAM_ID, + program_id=SystemAddresses.TOKEN_PROGRAM, data=data, accounts=accounts ) @@ -374,7 +374,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): data = bytes([9]) return Instruction( - program_id=address_provider.TOKEN_PROGRAM_ID, + program_id=SystemAddresses.TOKEN_PROGRAM, data=data, accounts=accounts ) diff --git a/src/platforms/pumpfun/address_provider.py b/src/platforms/pumpfun/address_provider.py index cb64349..9212a4e 100644 --- a/src/platforms/pumpfun/address_provider.py +++ b/src/platforms/pumpfun/address_provider.py @@ -5,14 +5,68 @@ This module provides all pump.fun-specific addresses and PDA derivations by implementing the AddressProvider interface. """ +from dataclasses import dataclass +from typing import Final from solders.pubkey import Pubkey from spl.token.instructions import get_associated_token_address -from core.pubkeys import PumpAddresses, SystemAddresses +from core.pubkeys import SystemAddresses from interfaces.core import AddressProvider, Platform, TokenInfo +@dataclass +class PumpFunAddresses: + """Pump.fun program addresses.""" + + PROGRAM: Final[Pubkey] = Pubkey.from_string( + "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" + ) + GLOBAL: Final[Pubkey] = Pubkey.from_string( + "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf" + ) + EVENT_AUTHORITY: Final[Pubkey] = Pubkey.from_string( + "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1" + ) + FEE: Final[Pubkey] = Pubkey.from_string( + "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM" + ) + LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string( + "39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg" + ) + + @staticmethod + 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 + """ + derived_address, _ = Pubkey.find_program_address( + [b"global_volume_accumulator"], + PumpFunAddresses.PROGRAM, + ) + return derived_address + + @staticmethod + 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 + """ + derived_address, _ = Pubkey.find_program_address( + [b"user_volume_accumulator", bytes(user)], + PumpFunAddresses.PROGRAM, + ) + return derived_address + + class PumpFunAddressProvider(AddressProvider): """Pump.Fun implementation of AddressProvider interface.""" @@ -24,7 +78,7 @@ class PumpFunAddressProvider(AddressProvider): @property def program_id(self) -> Pubkey: """Get the main program ID for this platform.""" - return PumpAddresses.PROGRAM + return PumpFunAddresses.PROGRAM def get_system_addresses(self) -> dict[str, Pubkey]: """Get all system addresses required for pump.fun. @@ -32,21 +86,21 @@ class PumpFunAddressProvider(AddressProvider): Returns: Dictionary mapping address names to Pubkey objects """ - return { + # 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 - "program": PumpAddresses.PROGRAM, - "global": PumpAddresses.GLOBAL, - "event_authority": PumpAddresses.EVENT_AUTHORITY, - "fee": PumpAddresses.FEE, - "liquidity_migrator": PumpAddresses.LIQUIDITY_MIGRATOR, - - # System addresses - "system_program": SystemAddresses.PROGRAM, - "token_program": SystemAddresses.TOKEN_PROGRAM, - "associated_token_program": SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, - "rent": SystemAddresses.RENT, - "sol_mint": SystemAddresses.SOL, + "program": PumpFunAddresses.PROGRAM, + "global": PumpFunAddresses.GLOBAL, + "event_authority": PumpFunAddresses.EVENT_AUTHORITY, + "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: """Derive the bonding curve address for a token. @@ -62,7 +116,7 @@ class PumpFunAddressProvider(AddressProvider): """ bonding_curve, _ = Pubkey.find_program_address( [b"bonding-curve", bytes(base_mint)], - PumpAddresses.PROGRAM + PumpFunAddresses.PROGRAM ) return bonding_curve @@ -136,7 +190,7 @@ class PumpFunAddressProvider(AddressProvider): """ creator_vault, _ = Pubkey.find_program_address( [b"creator-vault", bytes(creator)], - PumpAddresses.PROGRAM + PumpFunAddresses.PROGRAM ) return creator_vault @@ -146,7 +200,7 @@ class PumpFunAddressProvider(AddressProvider): Returns: Global volume accumulator address """ - return PumpAddresses.find_global_volume_accumulator() + return PumpFunAddresses.find_global_volume_accumulator() def derive_user_volume_accumulator(self, user: Pubkey) -> Pubkey: """Derive the user volume accumulator PDA. @@ -157,7 +211,7 @@ class PumpFunAddressProvider(AddressProvider): Returns: User volume accumulator address """ - return PumpAddresses.find_user_volume_accumulator(user) + return PumpFunAddresses.find_user_volume_accumulator(user) def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]: """Get all accounts needed for a buy instruction. @@ -172,18 +226,18 @@ class PumpFunAddressProvider(AddressProvider): additional_accounts = self.get_additional_accounts(token_info) return { - "global": PumpAddresses.GLOBAL, - "fee": PumpAddresses.FEE, + "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), "user_token_account": self.derive_user_token_account(user, token_info.mint), "user": user, - "system_program": SystemAddresses.PROGRAM, + "system_program": SystemAddresses.SYSTEM_PROGRAM, "token_program": SystemAddresses.TOKEN_PROGRAM, "creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault), - "event_authority": PumpAddresses.EVENT_AUTHORITY, - "program": PumpAddresses.PROGRAM, + "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), } @@ -201,16 +255,16 @@ class PumpFunAddressProvider(AddressProvider): additional_accounts = self.get_additional_accounts(token_info) return { - "global": PumpAddresses.GLOBAL, - "fee": PumpAddresses.FEE, + "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), "user_token_account": self.derive_user_token_account(user, token_info.mint), "user": user, - "system_program": SystemAddresses.PROGRAM, + "system_program": SystemAddresses.SYSTEM_PROGRAM, "creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault), "token_program": SystemAddresses.TOKEN_PROGRAM, - "event_authority": PumpAddresses.EVENT_AUTHORITY, - "program": PumpAddresses.PROGRAM, + "event_authority": PumpFunAddresses.EVENT_AUTHORITY, + "program": PumpFunAddresses.PROGRAM, } \ No newline at end of file diff --git a/src/platforms/pumpfun/event_parser.py b/src/platforms/pumpfun/event_parser.py index cccade0..cf26f09 100644 --- a/src/platforms/pumpfun/event_parser.py +++ b/src/platforms/pumpfun/event_parser.py @@ -14,8 +14,9 @@ import base58 from solders.pubkey import Pubkey from solders.transaction import VersionedTransaction -from core.pubkeys import PumpAddresses, SystemAddresses +from core.pubkeys import SystemAddresses from interfaces.core import EventParser, Platform, TokenInfo +from platforms.pumpfun.address_provider import PumpFunAddresses class PumpFunEventParser(EventParser): @@ -180,7 +181,7 @@ class PumpFunEventParser(EventParser): Returns: Pump.fun program ID """ - return PumpAddresses.PROGRAM + return PumpFunAddresses.PROGRAM def get_instruction_discriminators(self) -> list[bytes]: """Get instruction discriminators for token creation. @@ -310,7 +311,7 @@ class PumpFunEventParser(EventParser): """ derived_address, _ = Pubkey.find_program_address( [b"creator-vault", bytes(creator)], - PumpAddresses.PROGRAM, + PumpFunAddresses.PROGRAM, ) return derived_address From 04342fee5abbac9437b8a754e7f435911d08113f Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 14:17:29 +0000 Subject: [PATCH 05/30] fix(pump): fix curve management --- src/core/curve.py | 165 --------------------- src/platforms/pumpfun/curve_manager.py | 195 +++++++++++++++++++++---- 2 files changed, 167 insertions(+), 193 deletions(-) delete mode 100644 src/core/curve.py diff --git a/src/core/curve.py b/src/core/curve.py deleted file mode 100644 index d4ddf32..0000000 --- a/src/core/curve.py +++ /dev/null @@ -1,165 +0,0 @@ -""" -Pump.fun bonding curve manager for price calculations and curve state management. -""" - -import struct -from dataclasses import dataclass - -from solders.pubkey import Pubkey - -from core.client import SolanaClient -from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS -from utils.logger import get_logger - -logger = get_logger(__name__) - -# Bonding curve discriminator -CURVE_DISCRIMINATOR = bytes([23, 203, 71, 8, 209, 70, 227, 3]) - - -@dataclass -class BondingCurveState: - """Represents the state of a pump.fun bonding curve.""" - - virtual_token_reserves: int - virtual_sol_reserves: int - real_token_reserves: int - real_sol_reserves: int - token_total_supply: int - complete: bool - creator: Pubkey - - @property - def token_reserves(self) -> float: - """Token reserves in decimal form.""" - return self.virtual_token_reserves / 10**TOKEN_DECIMALS - - @property - def sol_reserves(self) -> float: - """SOL reserves in decimal form.""" - return self.virtual_sol_reserves / LAMPORTS_PER_SOL - - def calculate_price(self) -> float: - """Calculate current token price in SOL.""" - if self.virtual_token_reserves <= 0: - return 0.0 - - # Price = sol_reserves / token_reserves - price_lamports = self.virtual_sol_reserves / self.virtual_token_reserves - return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL - - -class BondingCurveManager: - """Manages pump.fun bonding curve operations.""" - - def __init__(self, client: SolanaClient): - """Initialize bonding curve manager. - - Args: - client: Solana RPC client - """ - self.client = client - - async def get_curve_state(self, curve_address: Pubkey) -> BondingCurveState: - """Get the current state of a bonding curve. - - Args: - curve_address: Address of the bonding curve - - Returns: - BondingCurveState with current curve data - - Raises: - ValueError: If curve data is invalid or inaccessible - """ - try: - account = await self.client.get_account_info(curve_address) - if not account.data: - raise ValueError(f"No data in bonding curve account {curve_address}") - - curve_state = self._decode_curve_state(account.data) - return curve_state - - except Exception as e: - logger.error(f"Failed to get curve state: {e!s}") - raise ValueError(f"Invalid bonding curve state: {e!s}") - - async def calculate_price(self, curve_address: Pubkey) -> float: - """Calculate current token price from bonding curve. - - Args: - curve_address: Address of the bonding curve - - Returns: - Current token price in SOL - """ - curve_state = await self.get_curve_state(curve_address) - return curve_state.calculate_price() - - def _decode_curve_state(self, data: bytes) -> BondingCurveState: - """Decode bonding curve state from raw account data. - - Args: - data: Raw account data - - Returns: - Decoded BondingCurveState - - Raises: - ValueError: If data format is invalid - """ - if len(data) < 8: - raise ValueError("Curve data too short") - - # Check discriminator - if not data.startswith(CURVE_DISCRIMINATOR): - raise ValueError("Invalid curve discriminator") - - offset = 8 - - try: - # Decode based on pump.fun BondingCurve structure: - # - virtual_token_reserves: u64 (8 bytes) - # - virtual_sol_reserves: u64 (8 bytes) - # - real_token_reserves: u64 (8 bytes) - # - real_sol_reserves: u64 (8 bytes) - # - token_total_supply: u64 (8 bytes) - # - complete: bool (1 byte) - # - padding: 7 bytes - # - creator: Pubkey (32 bytes) - - virtual_token_reserves = struct.unpack_from(" float: + """Token reserves in decimal form.""" + return self.virtual_token_reserves / 10**TOKEN_DECIMALS + + @property + def sol_reserves(self) -> float: + """SOL reserves in decimal form.""" + return self.virtual_sol_reserves / LAMPORTS_PER_SOL + + def calculate_price(self) -> float: + """Calculate current token price in SOL.""" + if self.virtual_token_reserves <= 0: + return 0.0 + + # Price = sol_reserves / token_reserves + price_lamports = self.virtual_sol_reserves / self.virtual_token_reserves + return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL class PumpFunCurveManager(CurveManager): @@ -25,7 +64,6 @@ class PumpFunCurveManager(CurveManager): client: Solana RPC client """ self.client = client - self.bonding_curve_manager = BondingCurveManager(client) @property def platform(self) -> Platform: @@ -41,22 +79,31 @@ class PumpFunCurveManager(CurveManager): Returns: Dictionary containing bonding curve state data """ - curve_state = await self.bonding_curve_manager.get_curve_state(pool_address) - - return { - "virtual_token_reserves": curve_state.virtual_token_reserves, - "virtual_sol_reserves": curve_state.virtual_sol_reserves, - "real_token_reserves": curve_state.real_token_reserves, - "real_sol_reserves": curve_state.real_sol_reserves, - "token_total_supply": curve_state.token_total_supply, - "complete": curve_state.complete, - "creator": str(curve_state.creator), + try: + account = await self.client.get_account_info(pool_address) + if not account.data: + raise ValueError(f"No data in bonding curve account {pool_address}") - # Calculated fields for convenience - "price_per_token": curve_state.calculate_price(), - "token_reserves_decimal": curve_state.token_reserves, - "sol_reserves_decimal": curve_state.sol_reserves, - } + curve_state = self._decode_curve_state(account.data) + + return { + "virtual_token_reserves": curve_state.virtual_token_reserves, + "virtual_sol_reserves": curve_state.virtual_sol_reserves, + "real_token_reserves": curve_state.real_token_reserves, + "real_sol_reserves": curve_state.real_sol_reserves, + "token_total_supply": curve_state.token_total_supply, + "complete": curve_state.complete, + "creator": str(curve_state.creator), + + # Calculated fields for convenience + "price_per_token": curve_state.calculate_price(), + "token_reserves_decimal": curve_state.token_reserves, + "sol_reserves_decimal": curve_state.sol_reserves, + } + + except Exception as e: + logger.error(f"Failed to get curve state: {e!s}") + 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. @@ -67,7 +114,8 @@ class PumpFunCurveManager(CurveManager): Returns: Current token price in SOL """ - return await self.bonding_curve_manager.calculate_price(pool_address) + curve_state = await self._get_curve_state_object(pool_address) + return curve_state.calculate_price() async def calculate_buy_amount_out( self, @@ -85,7 +133,7 @@ class PumpFunCurveManager(CurveManager): Returns: Expected amount of tokens to receive (in raw token units) """ - curve_state = await self.bonding_curve_manager.get_curve_state(pool_address) + curve_state = await self._get_curve_state_object(pool_address) # Use virtual reserves for bonding curve calculation # Formula: tokens_out = (amount_in * virtual_token_reserves) / (virtual_sol_reserves + amount_in) @@ -114,7 +162,7 @@ class PumpFunCurveManager(CurveManager): Returns: Expected amount of SOL to receive (in lamports) """ - curve_state = await self.bonding_curve_manager.get_curve_state(pool_address) + curve_state = await self._get_curve_state_object(pool_address) # Use virtual reserves for bonding curve calculation # Formula: sol_out = (amount_in * virtual_sol_reserves) / (virtual_token_reserves + amount_in) @@ -136,14 +184,106 @@ class PumpFunCurveManager(CurveManager): Returns: Tuple of (token_reserves, sol_reserves) in raw units """ - curve_state = await self.bonding_curve_manager.get_curve_state(pool_address) + curve_state = await self._get_curve_state_object(pool_address) return (curve_state.virtual_token_reserves, curve_state.virtual_sol_reserves) + async def _get_curve_state_object(self, curve_address: Pubkey) -> BondingCurveState: + """Get the current state of a bonding curve as a BondingCurveState object. + + Args: + curve_address: Address of the bonding curve + + Returns: + BondingCurveState with current curve data + + Raises: + ValueError: If curve data is invalid or inaccessible + """ + try: + account = await self.client.get_account_info(curve_address) + if not account.data: + raise ValueError(f"No data in bonding curve account {curve_address}") + + curve_state = self._decode_curve_state(account.data) + return curve_state + + except Exception as e: + logger.error(f"Failed to get curve state: {e!s}") + raise ValueError(f"Invalid bonding curve state: {e!s}") + + def _decode_curve_state(self, data: bytes) -> BondingCurveState: + """Decode bonding curve state from raw account data. + + Args: + data: Raw account data + + Returns: + Decoded BondingCurveState + + Raises: + ValueError: If data format is invalid + """ + if len(data) < 8: + raise ValueError("Curve data too short") + + # Check discriminator + if not data.startswith(CURVE_DISCRIMINATOR): + raise ValueError("Invalid curve discriminator") + + offset = 8 + + try: + # Decode based on pump.fun BondingCurve structure: + # - virtual_token_reserves: u64 (8 bytes) + # - virtual_sol_reserves: u64 (8 bytes) + # - real_token_reserves: u64 (8 bytes) + # - real_sol_reserves: u64 (8 bytes) + # - token_total_supply: u64 (8 bytes) + # - complete: bool (1 byte) + # - padding: 7 bytes + # - creator: Pubkey (32 bytes) + + virtual_token_reserves = struct.unpack_from(" float: """Calculate the expected token amount for a given SOL input. - This is a convenience method that wraps calculate_buy_amount_out - and converts between decimal and raw units. + This is a convenience method that converts between decimal and raw units. Args: pool_address: Address of the bonding curve @@ -159,8 +299,7 @@ class PumpFunCurveManager(CurveManager): 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 wraps calculate_sell_amount_out - and converts between decimal and raw units. + This is a convenience method that converts between decimal and raw units. Args: pool_address: Address of the bonding curve @@ -182,7 +321,7 @@ class PumpFunCurveManager(CurveManager): Returns: True if curve is complete, False otherwise """ - curve_state = await self.bonding_curve_manager.get_curve_state(pool_address) + curve_state = await self._get_curve_state_object(pool_address) return curve_state.complete async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]: @@ -194,7 +333,7 @@ class PumpFunCurveManager(CurveManager): Returns: Dictionary with progress information """ - curve_state = await self.bonding_curve_manager.get_curve_state(pool_address) + curve_state = await self._get_curve_state_object(pool_address) # Calculate progress based on SOL raised vs target # This is approximate since the exact target isn't stored in the curve state From f92f68fc41cd75fc43b7dedc4ddb3d2e5cc0ab57 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 14:21:16 +0000 Subject: [PATCH 06/30] wip(core): dynamic idl parser --- idl/raydium_launchlab_idl.json | 4304 ++++++++++++++++++++++++++++++++ src/utils/idl_parser.py | 356 +++ 2 files changed, 4660 insertions(+) create mode 100644 idl/raydium_launchlab_idl.json create mode 100644 src/utils/idl_parser.py diff --git a/idl/raydium_launchlab_idl.json b/idl/raydium_launchlab_idl.json new file mode 100644 index 0000000..e035478 --- /dev/null +++ b/idl/raydium_launchlab_idl.json @@ -0,0 +1,4304 @@ +{ + "address": "LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj", + "metadata": { + "name": "raydium_launchpad", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "buy_exact_in", + "docs": [ + "Use the given amount of quote tokens to purchase base tokens.", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `amount_in` - Amount of quote token to purchase", + "* `minimum_amount_out` - Minimum amount of base token to receive (slippage protection)", + "* `share_fee_rate` - Fee rate for the share", + "" + ], + "discriminator": [ + 250, + 234, + 13, + 123, + 213, + 156, + 19, + 236 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "The user performing the swap operation", + "Must sign the transaction and pay for fees" + ], + "signer": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "Global configuration account containing protocol-wide settings", + "Used to read protocol fee rates and curve type" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform-wide settings", + "Used to read platform fee rate" + ] + }, + { + "name": "pool_state", + "docs": [ + "The pool state account where the swap will be performed", + "Contains current pool parameters and balances" + ], + "writable": true + }, + { + "name": "user_base_token", + "docs": [ + "The user's token account for base tokens (tokens being bought)", + "Will receive the output tokens after the swap" + ], + "writable": true + }, + { + "name": "user_quote_token", + "docs": [ + "The user's token account for quote tokens (tokens being sold)", + "Will be debited for the input amount" + ], + "writable": true + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be debited to send tokens to the user" + ], + "writable": true + }, + { + "name": "quote_vault", + "docs": [ + "The pool's vault for quote tokens", + "Will receive the input tokens from the user" + ], + "writable": true + }, + { + "name": "base_token_mint", + "docs": [ + "The mint of the base token", + "Used for transfer fee calculations if applicable" + ] + }, + { + "name": "quote_token_mint", + "docs": [ + "The mint of the quote token" + ] + }, + { + "name": "base_token_program", + "docs": [ + "SPL Token program for base token transfers" + ] + }, + { + "name": "quote_token_program", + "docs": [ + "SPL Token program for quote token transfers" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "amount_in", + "type": "u64" + }, + { + "name": "minimum_amount_out", + "type": "u64" + }, + { + "name": "share_fee_rate", + "type": "u64" + } + ] + }, + { + "name": "buy_exact_out", + "docs": [ + "Use quote tokens to purchase the given amount of base tokens.", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `amount_out` - Amount of base token to receive", + "* `maximum_amount_in` - Maximum amount of quote token to purchase (slippage protection)", + "* `share_fee_rate` - Fee rate for the share" + ], + "discriminator": [ + 24, + 211, + 116, + 40, + 105, + 3, + 153, + 56 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "The user performing the swap operation", + "Must sign the transaction and pay for fees" + ], + "signer": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "Global configuration account containing protocol-wide settings", + "Used to read protocol fee rates and curve type" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform-wide settings", + "Used to read platform fee rate" + ] + }, + { + "name": "pool_state", + "docs": [ + "The pool state account where the swap will be performed", + "Contains current pool parameters and balances" + ], + "writable": true + }, + { + "name": "user_base_token", + "docs": [ + "The user's token account for base tokens (tokens being bought)", + "Will receive the output tokens after the swap" + ], + "writable": true + }, + { + "name": "user_quote_token", + "docs": [ + "The user's token account for quote tokens (tokens being sold)", + "Will be debited for the input amount" + ], + "writable": true + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be debited to send tokens to the user" + ], + "writable": true + }, + { + "name": "quote_vault", + "docs": [ + "The pool's vault for quote tokens", + "Will receive the input tokens from the user" + ], + "writable": true + }, + { + "name": "base_token_mint", + "docs": [ + "The mint of the base token", + "Used for transfer fee calculations if applicable" + ] + }, + { + "name": "quote_token_mint", + "docs": [ + "The mint of the quote token" + ] + }, + { + "name": "base_token_program", + "docs": [ + "SPL Token program for base token transfers" + ] + }, + { + "name": "quote_token_program", + "docs": [ + "SPL Token program for quote token transfers" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "amount_out", + "type": "u64" + }, + { + "name": "maximum_amount_in", + "type": "u64" + }, + { + "name": "share_fee_rate", + "type": "u64" + } + ] + }, + { + "name": "claim_platform_fee", + "docs": [ + "Claim platform fee", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 156, + 39, + 208, + 135, + 76, + 237, + 61, + 72 + ], + "accounts": [ + { + "name": "platform_fee_wallet", + "docs": [ + "Only the wallet stored in platform_config can collect platform fees" + ], + "writable": true, + "signer": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault and mint operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Account that stores the pool's state and parameters", + "PDA generated using POOL_SEED and both token mints" + ], + "writable": true + }, + { + "name": "platform_config", + "docs": [ + "The platform config account" + ] + }, + { + "name": "quote_vault", + "writable": true + }, + { + "name": "recipient_token_account", + "docs": [ + "The address that receives the collected quote token fees" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "platform_fee_wallet" + }, + { + "kind": "const", + "value": [ + 6, + 221, + 246, + 225, + 215, + 101, + 161, + 147, + 217, + 203, + 225, + 70, + 206, + 235, + 121, + 172, + 28, + 180, + 133, + 237, + 95, + 91, + 55, + 145, + 58, + 140, + 245, + 133, + 126, + 255, + 0, + 169 + ] + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "const", + "value": [ + 140, + 151, + 37, + 143, + 78, + 36, + 137, + 241, + 187, + 61, + 16, + 41, + 20, + 142, + 13, + 131, + 11, + 90, + 19, + 153, + 218, + 255, + 16, + 132, + 4, + 142, + 123, + 216, + 219, + 233, + 248, + 89 + ] + } + } + }, + { + "name": "quote_mint", + "docs": [ + "The mint of quote token vault" + ] + }, + { + "name": "token_program", + "docs": [ + "SPL program for input token transfers" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "docs": [ + "Required for associated token program" + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [] + }, + { + "name": "claim_vested_token", + "docs": [ + "Claim vested token", + "# Arguments" + ], + "discriminator": [ + 49, + 33, + 104, + 30, + 189, + 157, + 79, + 35 + ], + "accounts": [ + { + "name": "beneficiary", + "docs": [ + "The beneficiary of the vesting account" + ], + "writable": true, + "signer": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault and mint operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Account that stores the pool's state and parameters", + "PDA generated using POOL_SEED and both token mints" + ], + "writable": true + }, + { + "name": "vesting_record", + "docs": [ + "The vesting record account" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 101, + 115, + 116, + 105, + 110, + 103 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "beneficiary" + } + ] + } + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be debited to send tokens to the user" + ], + "writable": true + }, + { + "name": "user_base_token", + "writable": true, + "signer": true + }, + { + "name": "base_token_mint", + "docs": [ + "The mint for the base token (token being sold)", + "Created in this instruction with specified decimals" + ] + }, + { + "name": "base_token_program", + "docs": [ + "SPL Token program for the base token", + "Must be the standard Token program" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "associated_token_program", + "docs": [ + "Required for associated token program" + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + } + ], + "args": [] + }, + { + "name": "collect_fee", + "docs": [ + "Collects accumulated fees from the pool", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 60, + 173, + 247, + 103, + 4, + 93, + 130, + 48 + ], + "accounts": [ + { + "name": "owner", + "docs": [ + "Only protocol_fee_owner saved in global_config can collect protocol fee now" + ], + "signer": true + }, + { + "name": "authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Pool state stores accumulated protocol fee amount" + ], + "writable": true + }, + { + "name": "global_config", + "docs": [ + "Global config account stores owner" + ] + }, + { + "name": "quote_vault", + "docs": [ + "The address that holds pool tokens for quote token" + ], + "writable": true + }, + { + "name": "quote_mint", + "docs": [ + "The mint of quote token vault" + ] + }, + { + "name": "recipient_token_account", + "docs": [ + "The address that receives the collected quote token fees" + ], + "writable": true + }, + { + "name": "token_program", + "docs": [ + "SPL program for input token transfers" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [] + }, + { + "name": "collect_migrate_fee", + "docs": [ + "Collects migrate fees from the pool", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 255, + 186, + 150, + 223, + 235, + 118, + 201, + 186 + ], + "accounts": [ + { + "name": "owner", + "docs": [ + "Only migrate_fee_owner saved in global_config can collect migrate fee now" + ], + "signer": true + }, + { + "name": "authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Pool state stores accumulated protocol fee amount" + ], + "writable": true + }, + { + "name": "global_config", + "docs": [ + "Global config account stores owner" + ] + }, + { + "name": "quote_vault", + "docs": [ + "The address that holds pool tokens for quote token" + ], + "writable": true + }, + { + "name": "quote_mint", + "docs": [ + "The mint of quote token vault" + ] + }, + { + "name": "recipient_token_account", + "docs": [ + "The address that receives the collected quote token fees" + ], + "writable": true + }, + { + "name": "token_program", + "docs": [ + "SPL program for input token transfers" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [] + }, + { + "name": "create_config", + "docs": [ + "Creates a new configuration", + "# Arguments", + "", + "* `ctx` - The accounts needed by instruction", + "* `curve_type` - The type of bonding curve (0: ConstantProduct)", + "* `index` - The index of config, there may be multiple config with the same curve type.", + "* `trade_fee_rate` - Trade fee rate, must be less than RATE_DENOMINATOR_VALUE", + "" + ], + "discriminator": [ + 201, + 207, + 243, + 114, + 75, + 111, + 47, + 189 + ], + "accounts": [ + { + "name": "owner", + "docs": [ + "The protocol owner/admin account", + "Must match the predefined admin address", + "Has authority to create and modify protocol configurations" + ], + "writable": true, + "signer": true, + "address": "GThUX1Atko4tqhN2NaiTazWSeFWMuiUvfFnyJyUghFMJ" + }, + { + "name": "global_config", + "docs": [ + "Global configuration account that stores protocol-wide settings", + "PDA generated using GLOBAL_CONFIG_SEED, quote token mint, and curve type", + "Stores fee rates and protocol parameters" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 103, + 108, + 111, + 98, + 97, + 108, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "quote_token_mint" + }, + { + "kind": "arg", + "path": "curve_type" + }, + { + "kind": "arg", + "path": "index" + } + ] + } + }, + { + "name": "quote_token_mint", + "docs": [ + "The mint address of the quote token (token used for buying)", + "This will be the standard token used for all pools with this config" + ] + }, + { + "name": "protocol_fee_owner", + "docs": [ + "Account that will receive protocol fees" + ] + }, + { + "name": "migrate_fee_owner", + "docs": [ + "Account that will receive migrate fees" + ] + }, + { + "name": "migrate_to_amm_wallet", + "docs": [ + "The control wallet address for migrating to amm" + ] + }, + { + "name": "migrate_to_cpswap_wallet", + "docs": [ + "The control wallet address for migrating to cpswap" + ] + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "curve_type", + "type": "u8" + }, + { + "name": "index", + "type": "u16" + }, + { + "name": "migrate_fee", + "type": "u64" + }, + { + "name": "trade_fee_rate", + "type": "u64" + } + ] + }, + { + "name": "create_platform_config", + "docs": [ + "Create platform config account", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "# Fields", + "* `fee_rate` - Fee rate of the platform", + "* `name` - Name of the platform", + "* `web` - Website of the platform", + "* `img` - Image link of the platform", + "" + ], + "discriminator": [ + 176, + 90, + 196, + 175, + 253, + 113, + 220, + 20 + ], + "accounts": [ + { + "name": "platform_admin", + "docs": [ + "The account paying for the initialization costs" + ], + "writable": true, + "signer": true + }, + { + "name": "platform_fee_wallet" + }, + { + "name": "platform_nft_wallet" + }, + { + "name": "platform_config", + "docs": [ + "The platform config account" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "platform_admin" + } + ] + } + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "platform_params", + "type": { + "defined": { + "name": "PlatformParams" + } + } + } + ] + }, + { + "name": "create_vesting_account", + "docs": [ + "Create vesting account", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `share` - The share amount of base token to be vested", + "" + ], + "discriminator": [ + 129, + 178, + 2, + 13, + 217, + 172, + 230, + 218 + ], + "accounts": [ + { + "name": "creator", + "docs": [ + "The account paying for the initialization costs", + "This can be any account with sufficient SOL to cover the transaction" + ], + "writable": true, + "signer": true + }, + { + "name": "beneficiary", + "writable": true + }, + { + "name": "pool_state", + "docs": [ + "The pool state account" + ], + "writable": true + }, + { + "name": "vesting_record", + "docs": [ + "The vesting record account" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 101, + 115, + 116, + 105, + 110, + 103 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "beneficiary" + } + ] + } + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "share_amount", + "type": "u64" + } + ] + }, + { + "name": "initialize", + "docs": [ + "Initializes a new trading pool", + "# Arguments", + "", + "* `ctx` - The context of accounts containing pool and token information", + "" + ], + "discriminator": [ + 175, + 175, + 109, + 31, + 13, + 152, + 155, + 237 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "The account paying for the initialization costs", + "This can be any account with sufficient SOL to cover the transaction" + ], + "writable": true, + "signer": true + }, + { + "name": "creator" + }, + { + "name": "global_config", + "docs": [ + "Global configuration account containing protocol-wide settings", + "Includes settings like quote token mint and fee parameters" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform info", + "Includes settings like the fee_rate, name, web, img of the platform" + ] + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault and mint operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Account that stores the pool's state and parameters", + "PDA generated using POOL_SEED and both token mints" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108 + ] + }, + { + "kind": "account", + "path": "base_mint" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "base_mint", + "docs": [ + "The mint for the base token (token being sold)", + "Created in this instruction with specified decimals" + ], + "writable": true, + "signer": true + }, + { + "name": "quote_mint", + "docs": [ + "The mint for the quote token (token used to buy)", + "Must match the quote_mint specified in global config" + ] + }, + { + "name": "base_vault", + "docs": [ + "Token account that holds the pool's base tokens", + "PDA generated using POOL_VAULT_SEED" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "base_mint" + } + ] + } + }, + { + "name": "quote_vault", + "docs": [ + "Token account that holds the pool's quote tokens", + "PDA generated using POOL_VAULT_SEED" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "pool_state" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "metadata_account", + "docs": [ + "Account to store the base token's metadata", + "Created using Metaplex metadata program" + ], + "writable": true + }, + { + "name": "base_token_program", + "docs": [ + "SPL Token program for the base token", + "Must be the standard Token program" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "quote_token_program", + "docs": [ + "SPL Token program for the quote token" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "metadata_program", + "docs": [ + "Metaplex Token Metadata program", + "Used to create metadata for the base token" + ], + "address": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "rent_program", + "docs": [ + "Required for rent exempt calculations" + ], + "address": "SysvarRent111111111111111111111111111111111" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "base_mint_param", + "type": { + "defined": { + "name": "MintParams" + } + } + }, + { + "name": "curve_param", + "type": { + "defined": { + "name": "CurveParams" + } + } + }, + { + "name": "vesting_param", + "type": { + "defined": { + "name": "VestingParams" + } + } + } + ] + }, + { + "name": "migrate_to_amm", + "docs": [ + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 207, + 82, + 192, + 145, + 254, + 207, + 145, + 223 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "Only migrate_to_amm_wallet can migrate to cpswap pool", + "This signer must match the migrate_to_amm_wallet saved in global_config" + ], + "writable": true, + "signer": true + }, + { + "name": "base_mint", + "docs": [ + "The mint for the base token (token being sold)" + ] + }, + { + "name": "quote_mint", + "docs": [ + "The mint for the quote token (token used to buy)" + ] + }, + { + "name": "openbook_program", + "address": "srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX" + }, + { + "name": "market", + "docs": [ + "Account created and asigned to openbook_program but not been initialized" + ], + "writable": true + }, + { + "name": "request_queue", + "docs": [ + "Account created and asigned to openbook_program but not been initialized" + ], + "writable": true + }, + { + "name": "event_queue", + "docs": [ + "Account created and asigned to openbook_program but not been initialized" + ], + "writable": true + }, + { + "name": "bids", + "docs": [ + "Account created and asigned to openbook_program but not been initialized" + ], + "writable": true + }, + { + "name": "asks", + "docs": [ + "Account created and asigned to openbook_program but not been initialized" + ], + "writable": true + }, + { + "name": "market_vault_signer" + }, + { + "name": "market_base_vault", + "docs": [ + "Token account that holds the market's base tokens" + ], + "writable": true + }, + { + "name": "market_quote_vault", + "docs": [ + "Token account that holds the market's quote tokens" + ], + "writable": true + }, + { + "name": "amm_program", + "address": "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" + }, + { + "name": "amm_pool", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "amm_program" + }, + { + "kind": "account", + "path": "market" + }, + { + "kind": "const", + "value": [ + 97, + 109, + 109, + 95, + 97, + 115, + 115, + 111, + 99, + 105, + 97, + 116, + 101, + 100, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 97, + 109, + 109, + 32, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_open_orders", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "amm_program" + }, + { + "kind": "account", + "path": "market" + }, + { + "kind": "const", + "value": [ + 111, + 112, + 101, + 110, + 95, + 111, + 114, + 100, + 101, + 114, + 95, + 97, + 115, + 115, + 111, + 99, + 105, + 97, + 116, + 101, + 100, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_lp_mint", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "amm_program" + }, + { + "kind": "account", + "path": "market" + }, + { + "kind": "const", + "value": [ + 108, + 112, + 95, + 109, + 105, + 110, + 116, + 95, + 97, + 115, + 115, + 111, + 99, + 105, + 97, + 116, + 101, + 100, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_base_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "amm_program" + }, + { + "kind": "account", + "path": "market" + }, + { + "kind": "const", + "value": [ + 99, + 111, + 105, + 110, + 95, + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 115, + 115, + 111, + 99, + 105, + 97, + 116, + 101, + 100, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_quote_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "amm_program" + }, + { + "kind": "account", + "path": "market" + }, + { + "kind": "const", + "value": [ + 112, + 99, + 95, + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 115, + 115, + 111, + 99, + 105, + 97, + 116, + 101, + 100, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_target_orders", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "account", + "path": "amm_program" + }, + { + "kind": "account", + "path": "market" + }, + { + "kind": "const", + "value": [ + 116, + 97, + 114, + 103, + 101, + 116, + 95, + 97, + 115, + 115, + 111, + 99, + 105, + 97, + 116, + 101, + 100, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_config", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 97, + 109, + 109, + 95, + 99, + 111, + 110, + 102, + 105, + 103, + 95, + 97, + 99, + 99, + 111, + 117, + 110, + 116, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "amm_program" + } + } + }, + { + "name": "amm_create_fee_destination", + "writable": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault operations", + "Generated using AUTH_SEED" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Account that stores the pool's state and parameters", + "PDA generated using POOL_SEED and both token mints" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108 + ] + }, + { + "kind": "account", + "path": "base_mint" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "Global config account stores owner" + ] + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be fully drained during migration" + ], + "writable": true + }, + { + "name": "quote_vault", + "docs": [ + "The pool's vault for quote tokens", + "Will be fully drained during migration" + ], + "writable": true + }, + { + "name": "pool_lp_token", + "writable": true + }, + { + "name": "spl_token_program", + "docs": [ + "SPL Token program for the base token", + "Must be the standard Token program" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associated_token_program", + "docs": [ + "Program to create an ATA for receiving fee NFT" + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "rent_program", + "docs": [ + "Required for rent exempt calculations" + ], + "address": "SysvarRent111111111111111111111111111111111" + } + ], + "args": [ + { + "name": "base_lot_size", + "type": "u64" + }, + { + "name": "quote_lot_size", + "type": "u64" + }, + { + "name": "market_vault_signer_nonce", + "type": "u8" + } + ] + }, + { + "name": "migrate_to_cpswap", + "docs": [ + "# Arguments", + "", + "* `ctx` - The context of accounts", + "" + ], + "discriminator": [ + 136, + 92, + 200, + 103, + 28, + 218, + 144, + 140 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "Only migrate_to_cpswap_wallet can migrate to cpswap pool", + "This signer must match the migrate_to_cpswap_wallet saved in global_config" + ], + "writable": true, + "signer": true + }, + { + "name": "base_mint", + "docs": [ + "The mint for the base token (token being sold)" + ] + }, + { + "name": "quote_mint", + "docs": [ + "The mint for the quote token (token used to buy)" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform-wide settings", + "Used to read platform fee rate" + ] + }, + { + "name": "cpswap_program", + "address": "CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C" + }, + { + "name": "cpswap_pool", + "docs": [ + "PDA account:", + "seeds = [", + "b\"pool\",", + "cpswap_config.key().as_ref(),", + "token_0_mint.key().as_ref(),", + "token_1_mint.key().as_ref(),", + "],", + "seeds::program = cpswap_program,", + "", + "Or random account: must be signed by cli" + ], + "writable": true + }, + { + "name": "cpswap_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 110, + 100, + 95, + 108, + 112, + 95, + 109, + 105, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "cpswap_program" + } + } + }, + { + "name": "cpswap_lp_mint", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 108, + 112, + 95, + 109, + 105, + 110, + 116 + ] + }, + { + "kind": "account", + "path": "cpswap_pool" + } + ], + "program": { + "kind": "account", + "path": "cpswap_program" + } + } + }, + { + "name": "cpswap_base_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "cpswap_pool" + }, + { + "kind": "account", + "path": "base_mint" + } + ], + "program": { + "kind": "account", + "path": "cpswap_program" + } + } + }, + { + "name": "cpswap_quote_vault", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108, + 95, + 118, + 97, + 117, + 108, + 116 + ] + }, + { + "kind": "account", + "path": "cpswap_pool" + }, + { + "kind": "account", + "path": "quote_mint" + } + ], + "program": { + "kind": "account", + "path": "cpswap_program" + } + } + }, + { + "name": "cpswap_config" + }, + { + "name": "cpswap_create_pool_fee", + "writable": true + }, + { + "name": "cpswap_observation", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 111, + 98, + 115, + 101, + 114, + 118, + 97, + 116, + 105, + 111, + 110 + ] + }, + { + "kind": "account", + "path": "cpswap_pool" + } + ], + "program": { + "kind": "account", + "path": "cpswap_program" + } + } + }, + { + "name": "lock_program", + "address": "LockrWmn6K5twhz3y9w1dQERbmgSaRkfnTeTKbpofwE" + }, + { + "name": "lock_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 111, + 99, + 107, + 95, + 99, + 112, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121, + 95, + 115, + 101, + 101, + 100 + ] + } + ], + "program": { + "kind": "account", + "path": "lock_program" + } + } + }, + { + "name": "lock_lp_vault", + "writable": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault operations", + "Generated using AUTH_SEED" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "pool_state", + "docs": [ + "Account that stores the pool's state and parameters", + "PDA generated using POOL_SEED and both token mints" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 111, + 111, + 108 + ] + }, + { + "kind": "account", + "path": "base_mint" + }, + { + "kind": "account", + "path": "quote_mint" + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "Global config account stores owner" + ] + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be fully drained during migration" + ], + "writable": true + }, + { + "name": "quote_vault", + "docs": [ + "The pool's vault for quote tokens", + "Will be fully drained during migration" + ], + "writable": true + }, + { + "name": "pool_lp_token", + "writable": true + }, + { + "name": "base_token_program", + "docs": [ + "SPL Token program for the base token", + "Must be the standard Token program" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "quote_token_program", + "docs": [ + "SPL Token program for the quote token" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "associated_token_program", + "docs": [ + "Program to create an ATA for receiving fee NFT" + ], + "address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" + }, + { + "name": "system_program", + "docs": [ + "Required for account creation" + ], + "address": "11111111111111111111111111111111" + }, + { + "name": "rent_program", + "docs": [ + "Required for rent exempt calculations" + ], + "address": "SysvarRent111111111111111111111111111111111" + }, + { + "name": "metadata_program", + "docs": [ + "Program to create NFT metadata accunt" + ], + "address": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + } + ], + "args": [] + }, + { + "name": "sell_exact_in", + "docs": [ + "Use the given amount of base tokens to sell for quote tokens.", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `amount_in` - Amount of base token to sell", + "* `minimum_amount_out` - Minimum amount of quote token to receive (slippage protection)", + "* `share_fee_rate` - Fee rate for the share", + "" + ], + "discriminator": [ + 149, + 39, + 222, + 155, + 211, + 124, + 152, + 26 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "The user performing the swap operation", + "Must sign the transaction and pay for fees" + ], + "signer": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "Global configuration account containing protocol-wide settings", + "Used to read protocol fee rates and curve type" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform-wide settings", + "Used to read platform fee rate" + ] + }, + { + "name": "pool_state", + "docs": [ + "The pool state account where the swap will be performed", + "Contains current pool parameters and balances" + ], + "writable": true + }, + { + "name": "user_base_token", + "docs": [ + "The user's token account for base tokens (tokens being bought)", + "Will receive the output tokens after the swap" + ], + "writable": true + }, + { + "name": "user_quote_token", + "docs": [ + "The user's token account for quote tokens (tokens being sold)", + "Will be debited for the input amount" + ], + "writable": true + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be debited to send tokens to the user" + ], + "writable": true + }, + { + "name": "quote_vault", + "docs": [ + "The pool's vault for quote tokens", + "Will receive the input tokens from the user" + ], + "writable": true + }, + { + "name": "base_token_mint", + "docs": [ + "The mint of the base token", + "Used for transfer fee calculations if applicable" + ] + }, + { + "name": "quote_token_mint", + "docs": [ + "The mint of the quote token" + ] + }, + { + "name": "base_token_program", + "docs": [ + "SPL Token program for base token transfers" + ] + }, + { + "name": "quote_token_program", + "docs": [ + "SPL Token program for quote token transfers" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "amount_in", + "type": "u64" + }, + { + "name": "minimum_amount_out", + "type": "u64" + }, + { + "name": "share_fee_rate", + "type": "u64" + } + ] + }, + { + "name": "sell_exact_out", + "docs": [ + "Sell base tokens for the given amount of quote tokens.", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `amount_out` - Amount of quote token to receive", + "* `maximum_amount_in` - Maximum amount of base token to purchase (slippage protection)", + "* `share_fee_rate` - Fee rate for the share", + "" + ], + "discriminator": [ + 95, + 200, + 71, + 34, + 8, + 9, + 11, + 166 + ], + "accounts": [ + { + "name": "payer", + "docs": [ + "The user performing the swap operation", + "Must sign the transaction and pay for fees" + ], + "signer": true + }, + { + "name": "authority", + "docs": [ + "PDA that acts as the authority for pool vault operations", + "Generated using AUTH_SEED" + ], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 118, + 97, + 117, + 108, + 116, + 95, + 97, + 117, + 116, + 104, + 95, + 115, + 101, + 101, + 100 + ] + } + ] + } + }, + { + "name": "global_config", + "docs": [ + "Global configuration account containing protocol-wide settings", + "Used to read protocol fee rates and curve type" + ] + }, + { + "name": "platform_config", + "docs": [ + "Platform configuration account containing platform-wide settings", + "Used to read platform fee rate" + ] + }, + { + "name": "pool_state", + "docs": [ + "The pool state account where the swap will be performed", + "Contains current pool parameters and balances" + ], + "writable": true + }, + { + "name": "user_base_token", + "docs": [ + "The user's token account for base tokens (tokens being bought)", + "Will receive the output tokens after the swap" + ], + "writable": true + }, + { + "name": "user_quote_token", + "docs": [ + "The user's token account for quote tokens (tokens being sold)", + "Will be debited for the input amount" + ], + "writable": true + }, + { + "name": "base_vault", + "docs": [ + "The pool's vault for base tokens", + "Will be debited to send tokens to the user" + ], + "writable": true + }, + { + "name": "quote_vault", + "docs": [ + "The pool's vault for quote tokens", + "Will receive the input tokens from the user" + ], + "writable": true + }, + { + "name": "base_token_mint", + "docs": [ + "The mint of the base token", + "Used for transfer fee calculations if applicable" + ] + }, + { + "name": "quote_token_mint", + "docs": [ + "The mint of the quote token" + ] + }, + { + "name": "base_token_program", + "docs": [ + "SPL Token program for base token transfers" + ] + }, + { + "name": "quote_token_program", + "docs": [ + "SPL Token program for quote token transfers" + ], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + }, + { + "name": "event_authority", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 95, + 95, + 101, + 118, + 101, + 110, + 116, + 95, + 97, + 117, + 116, + 104, + 111, + 114, + 105, + 116, + 121 + ] + } + ] + } + }, + { + "name": "program" + } + ], + "args": [ + { + "name": "amount_out", + "type": "u64" + }, + { + "name": "maximum_amount_in", + "type": "u64" + }, + { + "name": "share_fee_rate", + "type": "u64" + } + ] + }, + { + "name": "update_config", + "docs": [ + "Updates configuration parameters", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `param` - Parameter to update:", + "- 0: Update trade_fee_rate", + "- 1: Update fee owner", + "* `value` - New value for the selected parameter", + "" + ], + "discriminator": [ + 29, + 158, + 252, + 191, + 10, + 83, + 219, + 99 + ], + "accounts": [ + { + "name": "owner", + "docs": [ + "The global config owner or admin" + ], + "signer": true, + "address": "GThUX1Atko4tqhN2NaiTazWSeFWMuiUvfFnyJyUghFMJ" + }, + { + "name": "global_config", + "docs": [ + "Global config account to be changed" + ], + "writable": true + } + ], + "args": [ + { + "name": "param", + "type": "u8" + }, + { + "name": "value", + "type": "u64" + } + ] + }, + { + "name": "update_platform_config", + "docs": [ + "Update platform config", + "# Arguments", + "", + "* `ctx` - The context of accounts", + "* `param` - Parameter to update", + "" + ], + "discriminator": [ + 195, + 60, + 76, + 129, + 146, + 45, + 67, + 143 + ], + "accounts": [ + { + "name": "platform_admin", + "docs": [ + "The account paying for the initialization costs" + ], + "signer": true + }, + { + "name": "platform_config", + "docs": [ + "Platform config account to be changed" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 112, + 108, + 97, + 116, + 102, + 111, + 114, + 109, + 95, + 99, + 111, + 110, + 102, + 105, + 103 + ] + }, + { + "kind": "account", + "path": "platform_admin" + } + ] + } + } + ], + "args": [ + { + "name": "param", + "type": { + "defined": { + "name": "PlatformConfigParam" + } + } + } + ] + } + ], + "accounts": [ + { + "name": "GlobalConfig", + "discriminator": [ + 149, + 8, + 156, + 202, + 160, + 252, + 176, + 217 + ] + }, + { + "name": "PlatformConfig", + "discriminator": [ + 160, + 78, + 128, + 0, + 248, + 83, + 230, + 160 + ] + }, + { + "name": "PoolState", + "discriminator": [ + 247, + 237, + 227, + 245, + 215, + 195, + 222, + 70 + ] + }, + { + "name": "VestingRecord", + "discriminator": [ + 106, + 243, + 221, + 205, + 230, + 126, + 85, + 83 + ] + } + ], + "events": [ + { + "name": "ClaimVestedEvent", + "discriminator": [ + 21, + 194, + 114, + 87, + 120, + 211, + 226, + 32 + ] + }, + { + "name": "CreateVestingEvent", + "discriminator": [ + 150, + 152, + 11, + 179, + 52, + 210, + 191, + 125 + ] + }, + { + "name": "PoolCreateEvent", + "discriminator": [ + 151, + 215, + 226, + 9, + 118, + 161, + 115, + 174 + ] + }, + { + "name": "TradeEvent", + "discriminator": [ + 189, + 219, + 127, + 211, + 78, + 230, + 97, + 238 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "NotApproved", + "msg": "Not approved" + }, + { + "code": 6001, + "name": "InvalidOwner", + "msg": "Input account owner is not the program address" + }, + { + "code": 6002, + "name": "InvalidInput", + "msg": "InvalidInput" + }, + { + "code": 6003, + "name": "InputNotMatchCurveConfig", + "msg": "The input params are not match with curve type in config" + }, + { + "code": 6004, + "name": "ExceededSlippage", + "msg": "Exceeds desired slippage limit" + }, + { + "code": 6005, + "name": "PoolFunding", + "msg": "Pool funding" + }, + { + "code": 6006, + "name": "PoolMigrated", + "msg": "Pool migrated" + }, + { + "code": 6007, + "name": "MigrateTypeNotMatch", + "msg": "Migrate type not match" + }, + { + "code": 6008, + "name": "MathOverflow", + "msg": "Math overflow" + }, + { + "code": 6009, + "name": "NoAssetsToCollect", + "msg": "No assets to collect" + }, + { + "code": 6010, + "name": "VestingRatioTooHigh", + "msg": "Vesting ratio too high" + }, + { + "code": 6011, + "name": "VestingSettingEnded", + "msg": "Vesting setting ended" + }, + { + "code": 6012, + "name": "VestingNotStarted", + "msg": "Vesting not started" + }, + { + "code": 6013, + "name": "NoVestingSchedule", + "msg": "No vesting schedule" + }, + { + "code": 6014, + "name": "InvalidPlatformInfo", + "msg": "The platform info input is invalid" + }, + { + "code": 6015, + "name": "PoolNotMigrated", + "msg": "Pool not migrated" + } + ], + "types": [ + { + "name": "ClaimVestedEvent", + "docs": [ + "Emitted when vesting token claimed by beneficiary" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool_state", + "type": "pubkey" + }, + { + "name": "beneficiary", + "type": "pubkey" + }, + { + "name": "claim_amount", + "type": "u64" + } + ] + } + }, + { + "name": "ConstantCurve", + "type": { + "kind": "struct", + "fields": [ + { + "name": "supply", + "type": "u64" + }, + { + "name": "total_base_sell", + "type": "u64" + }, + { + "name": "total_quote_fund_raising", + "type": "u64" + }, + { + "name": "migrate_type", + "type": "u8" + } + ] + } + }, + { + "name": "CreateVestingEvent", + "docs": [ + "Emitted when vest_account created" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool_state", + "type": "pubkey" + }, + { + "name": "beneficiary", + "type": "pubkey" + }, + { + "name": "share_amount", + "type": "u64" + } + ] + } + }, + { + "name": "CurveParams", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Constant", + "fields": [ + { + "name": "data", + "type": { + "defined": { + "name": "ConstantCurve" + } + } + } + ] + }, + { + "name": "Fixed", + "fields": [ + { + "name": "data", + "type": { + "defined": { + "name": "FixedCurve" + } + } + } + ] + }, + { + "name": "Linear", + "fields": [ + { + "name": "data", + "type": { + "defined": { + "name": "LinearCurve" + } + } + } + ] + } + ] + } + }, + { + "name": "FixedCurve", + "type": { + "kind": "struct", + "fields": [ + { + "name": "supply", + "type": "u64" + }, + { + "name": "total_quote_fund_raising", + "type": "u64" + }, + { + "name": "migrate_type", + "type": "u8" + } + ] + } + }, + { + "name": "GlobalConfig", + "docs": [ + "Holds the current owner of the factory" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch", + "docs": [ + "Account update epoch" + ], + "type": "u64" + }, + { + "name": "curve_type", + "docs": [ + "0: Constant Product Curve", + "1: Fixed Price Curve", + "2: Linear Price Curve" + ], + "type": "u8" + }, + { + "name": "index", + "docs": [ + "Config index" + ], + "type": "u16" + }, + { + "name": "migrate_fee", + "docs": [ + "The fee of migrate to amm" + ], + "type": "u64" + }, + { + "name": "trade_fee_rate", + "docs": [ + "The trade fee rate, denominated in hundredths of a bip (10^-6)" + ], + "type": "u64" + }, + { + "name": "max_share_fee_rate", + "docs": [ + "The maximum share fee rate, denominated in hundredths of a bip (10^-6)" + ], + "type": "u64" + }, + { + "name": "min_base_supply", + "docs": [ + "The minimum base supply, the value without decimals" + ], + "type": "u64" + }, + { + "name": "max_lock_rate", + "docs": [ + "The maximum lock rate, denominated in hundredths of a bip (10^-6)" + ], + "type": "u64" + }, + { + "name": "min_base_sell_rate", + "docs": [ + "The minimum base sell rate, denominated in hundredths of a bip (10^-6)" + ], + "type": "u64" + }, + { + "name": "min_base_migrate_rate", + "docs": [ + "The minimum base migrate rate, denominated in hundredths of a bip (10^-6)" + ], + "type": "u64" + }, + { + "name": "min_quote_fund_raising", + "docs": [ + "The minimum quote fund raising, the value with decimals" + ], + "type": "u64" + }, + { + "name": "quote_mint", + "docs": [ + "Mint information for quote token" + ], + "type": "pubkey" + }, + { + "name": "protocol_fee_owner", + "docs": [ + "Protocol Fee owner" + ], + "type": "pubkey" + }, + { + "name": "migrate_fee_owner", + "docs": [ + "Migrate Fee owner" + ], + "type": "pubkey" + }, + { + "name": "migrate_to_amm_wallet", + "docs": [ + "Migrate to amm control wallet" + ], + "type": "pubkey" + }, + { + "name": "migrate_to_cpswap_wallet", + "docs": [ + "Migrate to cpswap wallet" + ], + "type": "pubkey" + }, + { + "name": "padding", + "docs": [ + "padding for future updates" + ], + "type": { + "array": [ + "u64", + 16 + ] + } + } + ] + } + }, + { + "name": "LinearCurve", + "type": { + "kind": "struct", + "fields": [ + { + "name": "supply", + "type": "u64" + }, + { + "name": "total_quote_fund_raising", + "type": "u64" + }, + { + "name": "migrate_type", + "type": "u8" + } + ] + } + }, + { + "name": "MigrateNftInfo", + "docs": [ + "Represents the parameters for initializing a platform config account(Only support MigrateType::CPSWAP)", + "# Fields", + "* `platform_scale` - Scale of the platform liquidity quantity rights will be converted into NFT", + "* `creator_scale` - Scale of the token creator liquidity quantity rights will be converted into NFT", + "* `burn_scale` - Scale of liquidity directly to burn", + "", + "* platform_scale + creator_scale + burn_scale = RATE_DENOMINATOR_VALUE" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "platform_scale", + "type": "u64" + }, + { + "name": "creator_scale", + "type": "u64" + }, + { + "name": "burn_scale", + "type": "u64" + } + ] + } + }, + { + "name": "MintParams", + "docs": [ + "Represents the parameters for initializing a new token mint", + "# Fields", + "* `decimals` - Number of decimal places for the token", + "* `name` - Name of the token", + "* `symbol` - Symbol/ticker of the token", + "* `uri` - URI pointing to token metadata" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "decimals", + "type": "u8" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "uri", + "type": "string" + } + ] + } + }, + { + "name": "PlatformConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch", + "docs": [ + "The epoch for update interval" + ], + "type": "u64" + }, + { + "name": "platform_fee_wallet", + "docs": [ + "The platform fee wallet" + ], + "type": "pubkey" + }, + { + "name": "platform_nft_wallet", + "docs": [ + "The platform nft wallet to receive the platform NFT after migration if platform_scale is not 0(Only support MigrateType::CPSWAP)" + ], + "type": "pubkey" + }, + { + "name": "platform_scale", + "docs": [ + "Scale of the platform liquidity quantity rights will be converted into NFT(Only support MigrateType::CPSWAP)" + ], + "type": "u64" + }, + { + "name": "creator_scale", + "docs": [ + "Scale of the token creator liquidity quantity rights will be converted into NFT(Only support MigrateType::CPSWAP)" + ], + "type": "u64" + }, + { + "name": "burn_scale", + "docs": [ + "Scale of liquidity directly to burn" + ], + "type": "u64" + }, + { + "name": "fee_rate", + "docs": [ + "The platform fee rate" + ], + "type": "u64" + }, + { + "name": "name", + "docs": [ + "The platform name" + ], + "type": { + "array": [ + "u8", + 64 + ] + } + }, + { + "name": "web", + "docs": [ + "The platform website" + ], + "type": { + "array": [ + "u8", + 256 + ] + } + }, + { + "name": "img", + "docs": [ + "The platform img link" + ], + "type": { + "array": [ + "u8", + 256 + ] + } + }, + { + "name": "padding", + "docs": [ + "padding for future updates" + ], + "type": { + "array": [ + "u8", + 256 + ] + } + } + ] + } + }, + { + "name": "PlatformConfigParam", + "type": { + "kind": "enum", + "variants": [ + { + "name": "FeeWallet", + "fields": [ + "pubkey" + ] + }, + { + "name": "NFTWallet", + "fields": [ + "pubkey" + ] + }, + { + "name": "MigrateNftInfo", + "fields": [ + { + "defined": { + "name": "MigrateNftInfo" + } + } + ] + }, + { + "name": "FeeRate", + "fields": [ + "u64" + ] + }, + { + "name": "Name", + "fields": [ + "string" + ] + }, + { + "name": "Web", + "fields": [ + "string" + ] + }, + { + "name": "Img", + "fields": [ + "string" + ] + } + ] + } + }, + { + "name": "PlatformParams", + "docs": [ + "Represents the parameters for initializing a platform config account", + "# Fields", + "* `migrate_nft_info` - The platform configures liquidity info during migration(Only support MigrateType::CPSWAP)", + "* `fee_rate` - Fee rate of the platform", + "* `name` - Name of the platform", + "* `web` - Website of the platform", + "* `img` - Image link of the platform" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "migrate_nft_info", + "type": { + "defined": { + "name": "MigrateNftInfo" + } + } + }, + { + "name": "fee_rate", + "type": "u64" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "web", + "type": "string" + }, + { + "name": "img", + "type": "string" + } + ] + } + }, + { + "name": "PoolCreateEvent", + "docs": [ + "Emitted when pool created" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool_state", + "type": "pubkey" + }, + { + "name": "creator", + "type": "pubkey" + }, + { + "name": "config", + "type": "pubkey" + }, + { + "name": "base_mint_param", + "type": { + "defined": { + "name": "MintParams" + } + } + }, + { + "name": "curve_param", + "type": { + "defined": { + "name": "CurveParams" + } + } + }, + { + "name": "vesting_param", + "type": { + "defined": { + "name": "VestingParams" + } + } + } + ] + } + }, + { + "name": "PoolState", + "docs": [ + "Represents the state of a trading pool in the protocol", + "Stores all essential information about pool balances, fees, and configuration" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch", + "docs": [ + "Account update epoch" + ], + "type": "u64" + }, + { + "name": "auth_bump", + "docs": [ + "Bump seed used for PDA address derivation" + ], + "type": "u8" + }, + { + "name": "status", + "docs": [ + "Current status of the pool", + "* 0: Pool is funding", + "* 1: Pool funding is end, waiting for migration", + "* 2: Pool migration is done" + ], + "type": "u8" + }, + { + "name": "base_decimals", + "docs": [ + "Decimals of the pool base token" + ], + "type": "u8" + }, + { + "name": "quote_decimals", + "docs": [ + "Decimals of the pool quote token" + ], + "type": "u8" + }, + { + "name": "migrate_type", + "docs": [ + "Migrate to AMM or CpSwap" + ], + "type": "u8" + }, + { + "name": "supply", + "docs": [ + "Supply of the pool base token" + ], + "type": "u64" + }, + { + "name": "total_base_sell", + "docs": [ + "Total sell amount of the base token" + ], + "type": "u64" + }, + { + "name": "virtual_base", + "docs": [ + "For different curves, virtual_base and virtual_quote have different meanings", + "For constant product curve, virtual_base and virtual_quote are virtual liquidity, virtual_quote/virtual_base is the initial price", + "For linear price curve, virtual_base is the price slope parameter a, virtual_quote has no effect", + "For fixed price curve, virtual_quote/virtual_base is the initial price" + ], + "type": "u64" + }, + { + "name": "virtual_quote", + "type": "u64" + }, + { + "name": "real_base", + "docs": [ + "Actual base token amount in the pool", + "Represents the real tokens available for trading" + ], + "type": "u64" + }, + { + "name": "real_quote", + "docs": [ + "Actual quote token amount in the pool", + "Represents the real tokens available for trading" + ], + "type": "u64" + }, + { + "name": "total_quote_fund_raising", + "docs": [ + "The total quote fund raising of the pool" + ], + "type": "u64" + }, + { + "name": "quote_protocol_fee", + "docs": [ + "Accumulated trading fees in quote tokens", + "Can be collected by the protocol fee owner" + ], + "type": "u64" + }, + { + "name": "platform_fee", + "docs": [ + "Accumulated platform fees in quote tokens", + "Can be collected by the platform wallet stored in platform config" + ], + "type": "u64" + }, + { + "name": "migrate_fee", + "docs": [ + "The fee of migrate to amm" + ], + "type": "u64" + }, + { + "name": "vesting_schedule", + "docs": [ + "Vesting schedule for the base token" + ], + "type": { + "defined": { + "name": "VestingSchedule" + } + } + }, + { + "name": "global_config", + "docs": [ + "Public key of the global configuration account", + "Contains protocol-wide settings this pool adheres to" + ], + "type": "pubkey" + }, + { + "name": "platform_config", + "docs": [ + "Public key of the platform configuration account", + "Contains platform-wide settings this pool adheres to" + ], + "type": "pubkey" + }, + { + "name": "base_mint", + "docs": [ + "Public key of the base mint address" + ], + "type": "pubkey" + }, + { + "name": "quote_mint", + "docs": [ + "Public key of the quote mint address" + ], + "type": "pubkey" + }, + { + "name": "base_vault", + "docs": [ + "Public key of the base token vault", + "Holds the actual base tokens owned by the pool" + ], + "type": "pubkey" + }, + { + "name": "quote_vault", + "docs": [ + "Public key of the quote token vault", + "Holds the actual quote tokens owned by the pool" + ], + "type": "pubkey" + }, + { + "name": "creator", + "docs": [ + "The creator of base token" + ], + "type": "pubkey" + }, + { + "name": "padding", + "docs": [ + "padding for future updates" + ], + "type": { + "array": [ + "u64", + 8 + ] + } + } + ] + } + }, + { + "name": "PoolStatus", + "docs": [ + "Represents the different states a pool can be in", + "* Fund - Initial state where pool is accepting funds", + "* Migrate - Pool funding has ended and waiting for migration", + "* Trade - Pool migration is complete and amm trading is enabled" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "Fund" + }, + { + "name": "Migrate" + }, + { + "name": "Trade" + } + ] + } + }, + { + "name": "TradeDirection", + "docs": [ + "Specifies the direction of a trade in the bonding curve", + "This is important because curves can treat tokens differently through weights or offsets" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "Buy" + }, + { + "name": "Sell" + } + ] + } + }, + { + "name": "TradeEvent", + "docs": [ + "Emitted when trade process" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "pool_state", + "type": "pubkey" + }, + { + "name": "total_base_sell", + "type": "u64" + }, + { + "name": "virtual_base", + "type": "u64" + }, + { + "name": "virtual_quote", + "type": "u64" + }, + { + "name": "real_base_before", + "type": "u64" + }, + { + "name": "real_quote_before", + "type": "u64" + }, + { + "name": "real_base_after", + "type": "u64" + }, + { + "name": "real_quote_after", + "type": "u64" + }, + { + "name": "amount_in", + "type": "u64" + }, + { + "name": "amount_out", + "type": "u64" + }, + { + "name": "protocol_fee", + "type": "u64" + }, + { + "name": "platform_fee", + "type": "u64" + }, + { + "name": "share_fee", + "type": "u64" + }, + { + "name": "trade_direction", + "type": { + "defined": { + "name": "TradeDirection" + } + } + }, + { + "name": "pool_status", + "type": { + "defined": { + "name": "PoolStatus" + } + } + } + ] + } + }, + { + "name": "VestingParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "total_locked_amount", + "type": "u64" + }, + { + "name": "cliff_period", + "type": "u64" + }, + { + "name": "unlock_period", + "type": "u64" + } + ] + } + }, + { + "name": "VestingRecord", + "type": { + "kind": "struct", + "fields": [ + { + "name": "epoch", + "docs": [ + "Account update epoch" + ], + "type": "u64" + }, + { + "name": "pool", + "docs": [ + "The pool state account" + ], + "type": "pubkey" + }, + { + "name": "beneficiary", + "docs": [ + "The beneficiary of the vesting account" + ], + "type": "pubkey" + }, + { + "name": "claimed_amount", + "docs": [ + "The amount of tokens claimed" + ], + "type": "u64" + }, + { + "name": "token_share_amount", + "docs": [ + "The share amount of the token to be vested" + ], + "type": "u64" + }, + { + "name": "padding", + "docs": [ + "padding for future updates" + ], + "type": { + "array": [ + "u64", + 8 + ] + } + } + ] + } + }, + { + "name": "VestingSchedule", + "type": { + "kind": "struct", + "fields": [ + { + "name": "total_locked_amount", + "type": "u64" + }, + { + "name": "cliff_period", + "type": "u64" + }, + { + "name": "unlock_period", + "type": "u64" + }, + { + "name": "start_time", + "type": "u64" + }, + { + "name": "allocated_share_amount", + "docs": [ + "Total allocated share amount of the base token, not greater than total_locked_amount" + ], + "type": "u64" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/src/utils/idl_parser.py b/src/utils/idl_parser.py new file mode 100644 index 0000000..272f580 --- /dev/null +++ b/src/utils/idl_parser.py @@ -0,0 +1,356 @@ +""" +IDL Parser module for Solana programs. +Provides functionality to load and parse Anchor IDL files and decode instruction data. +""" + +import json +import struct +from typing import Any + +import base58 + +# Constants for Anchor data layout +DISCRIMINATOR_SIZE = 8 +PUBLIC_KEY_SIZE = 32 +STRING_LENGTH_PREFIX_SIZE = 4 +ENUM_DISCRIMINATOR_SIZE = 1 + + +class IDLParser: + """Parser for automatically decoding instructions using IDL definitions.""" + + # A single source of truth for primitive type information, mapping the type name + # 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()} + + def get_instruction_names(self) -> list[str]: + """Get a list of all available instruction names.""" + return [instr['name'] for instr in self.instructions.values()] + + 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 + + expected_min_size = self.instruction_min_sizes[discriminator] + actual_size = len(ix_data) + + if actual_size < expected_min_size: + instruction_name = self.instructions[discriminator]['name'] + if self.verbose: + print( + f"⚠️ Instruction data for '{instruction_name}' is shorter than the expected minimum " + f"({actual_size}/{expected_min_size} bytes)." + ) + return False + + return True + + 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 + + discriminator = ix_data[:DISCRIMINATOR_SIZE] + if discriminator not in self.instructions: + return None + + if not self.validate_instruction_data_length(ix_data, discriminator): + return None + + instruction = self.instructions[discriminator] + data_args = ix_data[DISCRIMINATOR_SIZE:] + + # Decode instruction arguments + args = {} + decode_offset = 0 + for arg in instruction.get('args', []): + try: + 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}") + return None + + # Helper to safely retrieve account public keys + def get_account_key(index: int) -> str | None: + 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 + + # Build account info based on instruction definition + account_info = {} + instruction_accounts = instruction.get('accounts', []) + for i, account_def in enumerate(instruction_accounts): + account_info[account_def['name']] = get_account_key(i) + + return { + 'instruction_name': instruction['name'], + 'args': args, + 'accounts': account_info + } + + 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. + """ + try: + if account_type_name not in self.types: + if self.verbose: + print(f"Account type '{account_type_name}' not found in IDL") + return None + + data = account_data + 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") + return None + data = account_data[DISCRIMINATOR_SIZE:] + + decoded_data, _ = self._decode_defined_type(data, 0, account_type_name) + return decoded_data + + except Exception as e: + if self.verbose: + print(f"Error decoding account data for {account_type_name}: {e}") + return None + + # -------------------------------------------------------------------------- + # Internal Helper Methods + # -------------------------------------------------------------------------- + + def _build_instruction_map(self): + """Build a map of discriminators to instruction definitions.""" + for instruction in self.idl.get('instructions', []): + # The discriminator from the JSON IDL is a list of u8 integers. + discriminator = bytes(instruction['discriminator']) + self.instructions[discriminator] = instruction + + 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 + + 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']) + self.instruction_min_sizes[discriminator] = min_size + if self.verbose and instruction['name'] == 'initialize': + print(f"📏 Initialize instruction min size: {min_size} bytes") + except Exception as e: + if self.verbose: + print(f"⚠️ Could not calculate size for {instruction['name']}: {e}") + self.instruction_min_sizes[discriminator] = DISCRIMINATOR_SIZE + + def _calculate_type_min_size(self, type_def: str | dict) -> int: + """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: + 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'] + 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}") + + def _get_primitive_size(self, type_name: str) -> int: + """Get size in bytes for primitive types from the central map.""" + info = self._PRIMITIVE_TYPE_INFO.get(type_name) + return info[1] if info else 0 + + 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'] + # New format: {'defined': {'name': 'MyType'}} + # Old format: {'defined': 'MyType'} + 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': + # 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']: + variant_size = 0 + 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 + 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']}") + + 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: + 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']) + + 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]: + """Decode fixed-size array types.""" + element_type, array_length = array_def + array_data = [] + for _ in range(array_length): + value, offset = self._decode_type(data, offset, element_type) + array_data.append(value) + return array_data, offset + + 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]: + """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': + struct_data = {} + 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}") + + variant = variants[variant_index] + 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 + 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 + + return result, offset + + raise ValueError(f"Unsupported type kind for decoding: {type_def['kind']}") + + +def load_idl_parser(idl_path: str, verbose: bool = False) -> IDLParser: + """ + Convenience function to load an IDL parser. + + Args: + idl_path: Path to the IDL JSON file + verbose: Whether to print debug information + + Returns: + Initialized IDLParser instance + """ + return IDLParser(idl_path, verbose) \ No newline at end of file From 4acabfb1af4ecf79a0b1c2f53b9aa0e8a770d60a Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 14:33:44 +0000 Subject: [PATCH 07/30] wip(letsbonk): idl parser in parser, builder --- src/platforms/letsbonk/event_parser.py | 141 +++++++----------- src/platforms/letsbonk/instruction_builder.py | 42 ++++-- 2 files changed, 89 insertions(+), 94 deletions(-) diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py index 8480647..2466503 100644 --- a/src/platforms/letsbonk/event_parser.py +++ b/src/platforms/letsbonk/event_parser.py @@ -2,34 +2,53 @@ LetsBonk implementation of EventParser interface. This module parses LetsBonk-specific token creation events from various sources -by implementing the EventParser interface. +by implementing the EventParser interface with IDL-based parsing. """ import base64 +import os import struct from time import monotonic -from typing import Any, Final +from typing import Any from solders.pubkey import Pubkey from solders.transaction import VersionedTransaction from interfaces.core import EventParser, Platform, TokenInfo from platforms.letsbonk.address_provider import LetsBonkAddressProvider +from utils.idl_parser import IDLParser from utils.logger import get_logger logger = get_logger(__name__) class LetsBonkEventParser(EventParser): - """LetsBonk implementation of EventParser interface.""" - - # Discriminator for initialize instruction from IDL - INITIALIZE_DISCRIMINATOR: Final[bytes] = bytes([175, 175, 109, 31, 13, 152, 155, 237]) - INITIALIZE_DISCRIMINATOR_INT: Final[int] = struct.unpack(" IDLParser: + """Load the IDL parser for LetsBonk (Raydium LaunchLab).""" + # Get the IDL file path relative to the project root + current_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.join(current_dir, "..", "..", "..") + idl_path = os.path.join(project_root, "idl", "raydium_launchlab_idl.json") + idl_path = os.path.normpath(idl_path) + + if not os.path.exists(idl_path): + raise FileNotFoundError(f"IDL file not found at {idl_path}") + + return IDLParser(idl_path, verbose=False) @property def platform(self) -> Platform: @@ -60,7 +79,7 @@ class LetsBonkEventParser(EventParser): accounts: list[int], account_keys: list[bytes] ) -> TokenInfo | None: - """Parse token creation from LetsBonk instruction data. + """Parse token creation from LetsBonk instruction data using IDL. Args: instruction_data: Raw instruction data @@ -70,7 +89,7 @@ class LetsBonkEventParser(EventParser): Returns: TokenInfo if token creation found, None otherwise """ - if not instruction_data.startswith(self.INITIALIZE_DISCRIMINATOR): + if not instruction_data.startswith(self._initialize_discriminator_bytes): return None try: @@ -83,9 +102,16 @@ class LetsBonkEventParser(EventParser): return None return Pubkey.from_bytes(account_keys[account_index]) - # Parse instruction data - token_data = self._parse_initialize_instruction_data(instruction_data) - if not token_data: + # Parse instruction data using IDL parser + 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', {}) + + # Extract MintParams from the decoded arguments + base_mint_param = args.get('base_mint_param', {}) + if not base_mint_param: return None # Extract account information based on IDL account order @@ -99,9 +125,9 @@ class LetsBonkEventParser(EventParser): return None return TokenInfo( - name=token_data["name"], - symbol=token_data["symbol"], - uri=token_data["uri"], + name=base_mint_param.get("name", ""), + symbol=base_mint_param.get("symbol", ""), + uri=base_mint_param.get("uri", ""), mint=base_mint, platform=Platform.LETS_BONK, pool_state=pool_state, @@ -112,7 +138,8 @@ class LetsBonkEventParser(EventParser): creation_timestamp=monotonic(), ) - except Exception: + except Exception as e: + logger.debug(f"Failed to parse initialize instruction: {e}") return None def parse_token_creation_from_geyser( @@ -151,7 +178,7 @@ 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.LETSBONK_PLATFORM_CONFIG): + if bytes(acc_key) == bytes(self.address_provider.get_system_addresses()["platform_config"]): has_platform_config = True break @@ -167,7 +194,8 @@ class LetsBonkEventParser(EventParser): return None - except Exception: + except Exception as e: + logger.debug(f"Failed to parse geyser transaction: {e}") return None def get_program_id(self) -> Pubkey: @@ -176,7 +204,7 @@ class LetsBonkEventParser(EventParser): Returns: Raydium LaunchLab program ID """ - return self.address_provider.RAYDIUM_LAUNCHLAB_PROGRAM_ID + return self.address_provider.program_id def get_instruction_discriminators(self) -> list[bytes]: """Get instruction discriminators for token creation. @@ -184,7 +212,7 @@ class LetsBonkEventParser(EventParser): Returns: List of discriminator bytes to match """ - return [self.INITIALIZE_DISCRIMINATOR] + 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). @@ -224,7 +252,7 @@ class LetsBonkEventParser(EventParser): if len(ix_data) >= 8: discriminator = struct.unpack("= 8: discriminator = struct.unpack(" dict | None: - """Parse the initialize instruction data from LetsBonk. - - Args: - data: Raw instruction data - - Returns: - Dictionary of parsed data or None if parsing fails - """ - if len(data) < 8: - return None - - # Check discriminator - discriminator = struct.unpack(" len(data): - raise ValueError("Not enough data for string length") - length = struct.unpack_from(" len(data): - raise ValueError("Not enough data for string") - value = data[offset:offset + length].decode('utf-8') - offset += length - return value - - def read_u8(): - nonlocal offset - if offset + 1 > len(data): - raise ValueError("Not enough data for u8") - value = struct.unpack_from(" IDLParser: + """Load the IDL parser for LetsBonk (Raydium LaunchLab).""" + # Get the IDL file path relative to the project root + current_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.join(current_dir, "..", "..", "..") + idl_path = os.path.join(project_root, "idl", "raydium_launchlab_idl.json") + idl_path = os.path.normpath(idl_path) + + if not os.path.exists(idl_path): + raise FileNotFoundError(f"IDL file not found at {idl_path}") + + return IDLParser(idl_path, verbose=False) @property def platform(self) -> Platform: @@ -119,7 +141,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate SHARE_FEE_RATE = 0 # No sharing fee instruction_data = ( - BUY_EXACT_IN_DISCRIMINATOR + + self._buy_exact_in_discriminator + struct.pack(" Date: Sat, 2 Aug 2025 14:38:54 +0000 Subject: [PATCH 08/30] feat(letsbonk): integrate idl parser --- src/platforms/letsbonk/curve_manager.py | 165 ++++++++++++------------ 1 file changed, 84 insertions(+), 81 deletions(-) diff --git a/src/platforms/letsbonk/curve_manager.py b/src/platforms/letsbonk/curve_manager.py index 243dae1..45540fc 100644 --- a/src/platforms/letsbonk/curve_manager.py +++ b/src/platforms/letsbonk/curve_manager.py @@ -5,7 +5,7 @@ This module handles LetsBonk (Raydium LaunchLab) specific pool operations by implementing the CurveManager interface using IDL-based decoding. """ -import struct +import os from typing import Any from solders.pubkey import Pubkey @@ -14,13 +14,11 @@ from core.client import SolanaClient from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS from interfaces.core import CurveManager, Platform from platforms.letsbonk.address_provider import LetsBonkAddressProvider +from utils.idl_parser import IDLParser from utils.logger import get_logger logger = get_logger(__name__) -# Pool state discriminator for Raydium LaunchLab -POOL_STATE_DISCRIMINATOR = bytes([247, 237, 227, 245, 215, 195, 222, 70]) - class LetsBonkCurveManager(CurveManager): """LetsBonk (Raydium LaunchLab) implementation of CurveManager interface.""" @@ -33,6 +31,22 @@ class LetsBonkCurveManager(CurveManager): """ self.client = client self.address_provider = LetsBonkAddressProvider() + self._idl_parser = self._load_idl_parser() + + logger.info("LetsBonk curve manager initialized with IDL-based account parsing") + + def _load_idl_parser(self) -> IDLParser: + """Load the IDL parser for LetsBonk (Raydium LaunchLab).""" + # Get the IDL file path relative to the project root + current_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.join(current_dir, "..", "..", "..") + idl_path = os.path.join(project_root, "idl", "raydium_launchlab_idl.json") + idl_path = os.path.normpath(idl_path) + + if not os.path.exists(idl_path): + raise FileNotFoundError(f"IDL file not found at {idl_path}") + + return IDLParser(idl_path, verbose=False) @property def platform(self) -> Platform: @@ -53,8 +67,8 @@ class LetsBonkCurveManager(CurveManager): if not account.data: raise ValueError(f"No data in pool state account {pool_address}") - # Decode pool state (simplified - in production you'd use IDL parser) - pool_state_data = self._decode_pool_state(account.data) + # Decode pool state using IDL parser + pool_state_data = self._decode_pool_state_with_idl(account.data) return pool_state_data @@ -160,95 +174,84 @@ class LetsBonkCurveManager(CurveManager): pool_state = await self.get_pool_state(pool_address) return (pool_state["virtual_base"], pool_state["virtual_quote"]) - def _decode_pool_state(self, data: bytes) -> dict[str, Any]: - """Decode pool state data from raw bytes. - - This is a simplified decoder. In production, you should use the IDL parser. + def _decode_pool_state_with_idl(self, data: bytes) -> dict[str, Any]: + """Decode pool state data using IDL parser. Args: data: Raw account data Returns: Dictionary with decoded pool state + + Raises: + ValueError: If IDL parsing fails """ - if len(data) < 8: - raise ValueError("Pool state data too short") + # Use IDL parser to decode PoolState account data + decoded_pool_state = self._idl_parser.decode_account_data( + data, + "PoolState", + skip_discriminator=True + ) - # Skip discriminator - offset = 8 + if not decoded_pool_state: + raise ValueError("Failed to decode pool state with IDL parser") - # Based on the PoolState structure from the IDL: - # - authority: Pubkey (32 bytes) - # - base_mint: Pubkey (32 bytes) - # - quote_mint: Pubkey (32 bytes) - # - base_vault: Pubkey (32 bytes) - # - quote_vault: Pubkey (32 bytes) - # - status: u8 (1 byte) - # - virtual_base: u64 (8 bytes) - # - virtual_quote: u64 (8 bytes) - # - real_base: u64 (8 bytes) - # - real_quote: u64 (8 bytes) - # ... and more fields + # Extract the fields we need for trading calculations + # Based on the PoolState structure from the IDL + pool_data = { + "virtual_base": decoded_pool_state.get("virtual_base", 0), + "virtual_quote": decoded_pool_state.get("virtual_quote", 0), + "real_base": decoded_pool_state.get("real_base", 0), + "real_quote": decoded_pool_state.get("real_quote", 0), + "status": decoded_pool_state.get("status", 0), + "supply": decoded_pool_state.get("supply", 0), + } - try: - # Skip to the fields we need - offset += 32 * 5 # Skip 5 pubkeys (authority, mints, vaults) - offset += 1 # Skip status - - # Read virtual reserves - virtual_base = struct.unpack_from(" 0 else 0, - } - - except Exception as e: - logger.error(f"Failed to decode pool state: {e}") - # Return some default values for testing - return { - "virtual_base": 1_000_000_000, # 1000 tokens with 6 decimals - "virtual_quote": 1_000_000_000, # 1 SOL - "real_base": 1_000_000_000, - "real_quote": 1_000_000_000, - "price_per_token": 0.001, # 0.001 SOL per token - } + # 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 + ) + 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") + + return pool_data - async def get_pool_info(self, pool_address: Pubkey) -> dict[str, Any]: - """Get detailed pool information including status and progress. + 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: - Dictionary with pool information + True if structure is valid, False otherwise """ - pool_state = await self.get_pool_state(pool_address) - - # Calculate additional metrics - sol_raised = pool_state["real_quote"] / LAMPORTS_PER_SOL - tokens_sold = (pool_state["virtual_base"] - pool_state["real_base"]) / 10**TOKEN_DECIMALS - - return { - "virtual_base_reserves": pool_state["virtual_base"], - "virtual_quote_reserves": pool_state["virtual_quote"], - "real_base_reserves": pool_state["real_base"], - "real_quote_reserves": pool_state["real_quote"], - "sol_raised": sol_raised, - "tokens_sold": tokens_sold, - "current_price": pool_state["price_per_token"], - } \ No newline at end of file + try: + # 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_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])}") + return False + + return True + + except Exception as e: + logger.error(f"Pool state validation failed: {e}") + return False \ No newline at end of file From 9a09862f0dc79d93049c351e867b617aeaca0e27 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 14:52:40 +0000 Subject: [PATCH 09/30] feat(letsbonk): add idl manager --- src/platforms/__init__.py | 66 +++++- src/platforms/letsbonk/curve_manager.py | 29 +-- src/platforms/letsbonk/event_parser.py | 34 ++- src/platforms/letsbonk/instruction_builder.py | 28 +-- src/utils/idl_manager.py | 195 ++++++++++++++++++ 5 files changed, 282 insertions(+), 70 deletions(-) create mode 100644 src/utils/idl_manager.py diff --git a/src/platforms/__init__.py b/src/platforms/__init__.py index 7c0db22..feb53f0 100644 --- a/src/platforms/__init__.py +++ b/src/platforms/__init__.py @@ -2,7 +2,7 @@ Platform factory and registry for managing multiple trading platforms. This module provides a centralized way to instantiate and access -platform-specific implementations of the trading interfaces. +platform-specific implementations of the trading interfaces with IDL support. """ from dataclasses import dataclass @@ -16,6 +16,10 @@ from interfaces.core import ( InstructionBuilder, Platform, ) +from utils.idl_manager import get_idl_manager, has_idl_support +from utils.logger import get_logger + +logger = get_logger(__name__) @dataclass @@ -64,7 +68,7 @@ class PlatformRegistry: client: SolanaClient, **kwargs: Any ) -> PlatformImplementations: - """Create platform implementation instances. + """Create platform implementation instances with IDL support. Args: platform: Platform to create implementations for @@ -89,11 +93,29 @@ class PlatformRegistry: impl_classes = self._implementations[platform] - # Create instances - only curve_manager needs client + # 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") + 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']() - instruction_builder = impl_classes['instruction_builder']() - curve_manager = impl_classes['curve_manager'](client) - event_parser = impl_classes['event_parser']() + + # For platforms with IDL support, pass the parser to relevant classes + if idl_parser and platform in [Platform.LETS_BONK]: # Add other IDL-supported platforms here + 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 (like pump.fun) + 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, @@ -138,10 +160,25 @@ class PlatformRegistry: 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 + """ + if platform is None: + 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] + for key in keys_to_remove: + del self._instances[key] + logger.info(f"Cleared cached implementations for {platform.value}") class PlatformFactory: - """Factory for creating platform-specific implementations.""" + """Factory for creating platform-specific implementations with IDL support.""" def __init__(self): self.registry = PlatformRegistry() @@ -200,7 +237,7 @@ class PlatformFactory: Args: platform: Platform to create implementations for client: Solana RPC client - **config: Platform-specific configuration + **config: Platform-specific configuration (including verbose_idl) Returns: PlatformImplementations containing all interface implementations @@ -266,6 +303,19 @@ class PlatformFactory: 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) # Global factory instance diff --git a/src/platforms/letsbonk/curve_manager.py b/src/platforms/letsbonk/curve_manager.py index 45540fc..395fe6e 100644 --- a/src/platforms/letsbonk/curve_manager.py +++ b/src/platforms/letsbonk/curve_manager.py @@ -5,7 +5,6 @@ This module handles LetsBonk (Raydium LaunchLab) specific pool operations by implementing the CurveManager interface using IDL-based decoding. """ -import os from typing import Any from solders.pubkey import Pubkey @@ -23,30 +22,18 @@ logger = get_logger(__name__) class LetsBonkCurveManager(CurveManager): """LetsBonk (Raydium LaunchLab) implementation of CurveManager interface.""" - def __init__(self, client: SolanaClient): - """Initialize LetsBonk curve manager. + 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 """ self.client = client self.address_provider = LetsBonkAddressProvider() - self._idl_parser = self._load_idl_parser() + self._idl_parser = idl_parser - logger.info("LetsBonk curve manager initialized with IDL-based account parsing") - - def _load_idl_parser(self) -> IDLParser: - """Load the IDL parser for LetsBonk (Raydium LaunchLab).""" - # Get the IDL file path relative to the project root - current_dir = os.path.dirname(os.path.abspath(__file__)) - project_root = os.path.join(current_dir, "..", "..", "..") - idl_path = os.path.join(project_root, "idl", "raydium_launchlab_idl.json") - idl_path = os.path.normpath(idl_path) - - if not os.path.exists(idl_path): - raise FileNotFoundError(f"IDL file not found at {idl_path}") - - return IDLParser(idl_path, verbose=False) + logger.info("LetsBonk curve manager initialized with injected IDL parser") @property def platform(self) -> Platform: @@ -67,7 +54,7 @@ class LetsBonkCurveManager(CurveManager): if not account.data: raise ValueError(f"No data in pool state account {pool_address}") - # Decode pool state using IDL parser + # Decode pool state using injected IDL parser pool_state_data = self._decode_pool_state_with_idl(account.data) return pool_state_data @@ -175,7 +162,7 @@ class LetsBonkCurveManager(CurveManager): 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 IDL parser. + """Decode pool state data using injected IDL parser. Args: data: Raw account data @@ -186,7 +173,7 @@ class LetsBonkCurveManager(CurveManager): Raises: ValueError: If IDL parsing fails """ - # Use IDL parser to decode PoolState account data + # Use injected IDL parser to decode PoolState account data decoded_pool_state = self._idl_parser.decode_account_data( data, "PoolState", diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py index 2466503..c5f6f90 100644 --- a/src/platforms/letsbonk/event_parser.py +++ b/src/platforms/letsbonk/event_parser.py @@ -6,7 +6,6 @@ by implementing the EventParser interface with IDL-based parsing. """ import base64 -import os import struct from time import monotonic from typing import Any @@ -25,30 +24,21 @@ logger = get_logger(__name__) class LetsBonkEventParser(EventParser): """LetsBonk implementation of EventParser interface with IDL-based parsing.""" - def __init__(self): - """Initialize LetsBonk event parser with IDL support.""" - self.address_provider = LetsBonkAddressProvider() - self._idl_parser = self._load_idl_parser() + def __init__(self, idl_parser: IDLParser): + """Initialize LetsBonk event parser with injected IDL parser. - # Get discriminators from IDL + 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(" IDLParser: - """Load the IDL parser for LetsBonk (Raydium LaunchLab).""" - # Get the IDL file path relative to the project root - current_dir = os.path.dirname(os.path.abspath(__file__)) - project_root = os.path.join(current_dir, "..", "..", "..") - idl_path = os.path.join(project_root, "idl", "raydium_launchlab_idl.json") - idl_path = os.path.normpath(idl_path) - - if not os.path.exists(idl_path): - raise FileNotFoundError(f"IDL file not found at {idl_path}") - - return IDLParser(idl_path, verbose=False) + logger.info("LetsBonk event parser initialized with injected IDL parser") @property def platform(self) -> Platform: @@ -79,7 +69,7 @@ class LetsBonkEventParser(EventParser): accounts: list[int], account_keys: list[bytes] ) -> TokenInfo | None: - """Parse token creation from LetsBonk instruction data using IDL. + """Parse token creation from LetsBonk instruction data using injected IDL parser. Args: instruction_data: Raw instruction data @@ -102,7 +92,7 @@ class LetsBonkEventParser(EventParser): return None return Pubkey.from_bytes(account_keys[account_index]) - # Parse instruction data using IDL parser + # 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': return None diff --git a/src/platforms/letsbonk/instruction_builder.py b/src/platforms/letsbonk/instruction_builder.py index 2e3c44b..b16a6bd 100644 --- a/src/platforms/letsbonk/instruction_builder.py +++ b/src/platforms/letsbonk/instruction_builder.py @@ -6,7 +6,6 @@ by implementing the InstructionBuilder interface with IDL-based discriminators. """ import hashlib -import os import struct import time @@ -26,29 +25,20 @@ logger = get_logger(__name__) class LetsBonkInstructionBuilder(InstructionBuilder): """LetsBonk (Raydium LaunchLab) implementation of InstructionBuilder interface with IDL-based discriminators.""" - def __init__(self): - """Initialize LetsBonk instruction builder with IDL support.""" - self._idl_parser = self._load_idl_parser() + def __init__(self, idl_parser: IDLParser): + """Initialize LetsBonk instruction builder with injected IDL parser. - # Get discriminators from IDL + Args: + idl_parser: Pre-loaded IDL parser for LetsBonk platform + """ + self._idl_parser = idl_parser + + # Get discriminators from injected IDL parser discriminators = self._idl_parser.get_instruction_discriminators() self._buy_exact_in_discriminator = discriminators["buy_exact_in"] self._sell_exact_in_discriminator = discriminators["sell_exact_in"] - logger.info("LetsBonk instruction builder initialized with IDL-based discriminators") - - def _load_idl_parser(self) -> IDLParser: - """Load the IDL parser for LetsBonk (Raydium LaunchLab).""" - # Get the IDL file path relative to the project root - current_dir = os.path.dirname(os.path.abspath(__file__)) - project_root = os.path.join(current_dir, "..", "..", "..") - idl_path = os.path.join(project_root, "idl", "raydium_launchlab_idl.json") - idl_path = os.path.normpath(idl_path) - - if not os.path.exists(idl_path): - raise FileNotFoundError(f"IDL file not found at {idl_path}") - - return IDLParser(idl_path, verbose=False) + logger.info("LetsBonk instruction builder initialized with injected IDL parser") @property def platform(self) -> Platform: diff --git a/src/utils/idl_manager.py b/src/utils/idl_manager.py new file mode 100644 index 0000000..bfb208c --- /dev/null +++ b/src/utils/idl_manager.py @@ -0,0 +1,195 @@ +""" +Centralized IDL management for Solana platforms. + +This module provides a single point of IDL loading and management to avoid +duplicate loading across multiple platform implementation classes. +""" + +import os + +from interfaces.core import Platform +from utils.idl_parser import IDLParser +from utils.logger import get_logger + +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_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.join(current_dir, "..", "..") + project_root = os.path.normpath(project_root) + + # Define IDL paths for each platform + self._idl_paths = { + Platform.LETS_BONK: os.path.join(project_root, "idl", "raydium_launchlab_idl.json"), + # Platform.PUMP_FUN: os.path.join(project_root, "idl", "pump_fun_idl.json"), # Future + } + + 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") + + idl_path = self._idl_paths[platform] + + # Verify IDL file exists + if not os.path.exists(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 + + logger.info(f"IDL parser loaded for {platform.value} with {len(parser.get_instruction_names())} instructions") + + 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 + """ + if platform is None: + logger.info("Clearing all cached IDL parsers") + self._parsers.clear() + 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 + """ + if platform not in self._parsers: + logger.info(f"Preloading IDL parser for {platform.value}") + self.get_parser(platform, verbose) + else: + logger.debug(f"IDL parser for {platform.value} already loaded") + + 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() + + +# Global IDL manager instance +_idl_manager: IDLManager | None = None + + +def get_idl_manager() -> IDLManager: + """Get the global IDL manager instance. + + Returns: + Global IDLManager instance + """ + global _idl_manager + if _idl_manager is None: + _idl_manager = IDLManager() + return _idl_manager + + +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 + """ + return get_idl_manager().get_parser(platform, verbose) + + +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 + """ + return get_idl_manager().has_idl_support(platform) + + +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 + """ + get_idl_manager().preload_parser(platform, verbose) \ No newline at end of file From 93332794a3a9cd8ed9d1dcf14d21c7e64068fc23 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 15:10:24 +0000 Subject: [PATCH 10/30] feat(pump): integrate idl parser manager --- src/platforms/__init__.py | 4 +- src/platforms/pumpfun/curve_manager.py | 272 ++++++++----------- src/platforms/pumpfun/event_parser.py | 227 ++++++++++------ src/platforms/pumpfun/instruction_builder.py | 30 +- src/utils/idl_manager.py | 2 +- 5 files changed, 284 insertions(+), 251 deletions(-) diff --git a/src/platforms/__init__.py b/src/platforms/__init__.py index feb53f0..f7dd9b5 100644 --- a/src/platforms/__init__.py +++ b/src/platforms/__init__.py @@ -107,12 +107,12 @@ class PlatformRegistry: 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]: # Add other IDL-supported platforms here + 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) else: - # Fallback for platforms without IDL support (like pump.fun) + # 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']() diff --git a/src/platforms/pumpfun/curve_manager.py b/src/platforms/pumpfun/curve_manager.py index 74220fb..b113c3c 100644 --- a/src/platforms/pumpfun/curve_manager.py +++ b/src/platforms/pumpfun/curve_manager.py @@ -2,11 +2,9 @@ Pump.Fun implementation of CurveManager interface. This module handles pump.fun-specific bonding curve operations -by implementing the CurveManager interface with all curve logic self-contained. +by implementing the CurveManager interface using IDL-based decoding. """ -import struct -from dataclasses import dataclass from typing import Any from solders.pubkey import Pubkey @@ -14,56 +12,26 @@ from solders.pubkey import Pubkey from core.client import SolanaClient from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS from interfaces.core import CurveManager, Platform +from utils.idl_parser import IDLParser from utils.logger import get_logger logger = get_logger(__name__) -# Bonding curve discriminator for pump.fun -CURVE_DISCRIMINATOR = bytes([23, 203, 71, 8, 209, 70, 227, 3]) - - -@dataclass -class BondingCurveState: - """Represents the state of a pump.fun bonding curve.""" - - virtual_token_reserves: int - virtual_sol_reserves: int - real_token_reserves: int - real_sol_reserves: int - token_total_supply: int - complete: bool - creator: Pubkey - - @property - def token_reserves(self) -> float: - """Token reserves in decimal form.""" - return self.virtual_token_reserves / 10**TOKEN_DECIMALS - - @property - def sol_reserves(self) -> float: - """SOL reserves in decimal form.""" - return self.virtual_sol_reserves / LAMPORTS_PER_SOL - - def calculate_price(self) -> float: - """Calculate current token price in SOL.""" - if self.virtual_token_reserves <= 0: - return 0.0 - - # Price = sol_reserves / token_reserves - price_lamports = self.virtual_sol_reserves / self.virtual_token_reserves - return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL - class PumpFunCurveManager(CurveManager): - """Pump.Fun implementation of CurveManager interface.""" + """Pump.Fun implementation of CurveManager interface using IDL-based decoding.""" - def __init__(self, client: SolanaClient): - """Initialize pump.fun curve manager. + 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: @@ -84,22 +52,10 @@ class PumpFunCurveManager(CurveManager): if not account.data: raise ValueError(f"No data in bonding curve account {pool_address}") - curve_state = self._decode_curve_state(account.data) + # Decode bonding curve state using injected IDL parser + curve_state_data = self._decode_curve_state_with_idl(account.data) - return { - "virtual_token_reserves": curve_state.virtual_token_reserves, - "virtual_sol_reserves": curve_state.virtual_sol_reserves, - "real_token_reserves": curve_state.real_token_reserves, - "real_sol_reserves": curve_state.real_sol_reserves, - "token_total_supply": curve_state.token_total_supply, - "complete": curve_state.complete, - "creator": str(curve_state.creator), - - # Calculated fields for convenience - "price_per_token": curve_state.calculate_price(), - "token_reserves_decimal": curve_state.token_reserves, - "sol_reserves_decimal": curve_state.sol_reserves, - } + return curve_state_data except Exception as e: logger.error(f"Failed to get curve state: {e!s}") @@ -114,8 +70,18 @@ class PumpFunCurveManager(CurveManager): Returns: Current token price in SOL """ - curve_state = await self._get_curve_state_object(pool_address) - return curve_state.calculate_price() + 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, @@ -133,12 +99,15 @@ class PumpFunCurveManager(CurveManager): Returns: Expected amount of tokens to receive (in raw token units) """ - curve_state = await self._get_curve_state_object(pool_address) + 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 * curve_state.virtual_token_reserves - denominator = curve_state.virtual_sol_reserves + amount_in + numerator = amount_in * virtual_token_reserves + denominator = virtual_sol_reserves + amount_in if denominator == 0: return 0 @@ -162,12 +131,15 @@ class PumpFunCurveManager(CurveManager): Returns: Expected amount of SOL to receive (in lamports) """ - curve_state = await self._get_curve_state_object(pool_address) + 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 * curve_state.virtual_sol_reserves - denominator = curve_state.virtual_token_reserves + amount_in + numerator = amount_in * virtual_sol_reserves + denominator = virtual_token_reserves + amount_in if denominator == 0: return 0 @@ -184,100 +156,61 @@ class PumpFunCurveManager(CurveManager): Returns: Tuple of (token_reserves, sol_reserves) in raw units """ - curve_state = await self._get_curve_state_object(pool_address) - return (curve_state.virtual_token_reserves, curve_state.virtual_sol_reserves) + pool_state = await self.get_pool_state(pool_address) + return (pool_state["virtual_token_reserves"], pool_state["virtual_sol_reserves"]) - async def _get_curve_state_object(self, curve_address: Pubkey) -> BondingCurveState: - """Get the current state of a bonding curve as a BondingCurveState object. - - Args: - curve_address: Address of the bonding curve - - Returns: - BondingCurveState with current curve data - - Raises: - ValueError: If curve data is invalid or inaccessible - """ - try: - account = await self.client.get_account_info(curve_address) - if not account.data: - raise ValueError(f"No data in bonding curve account {curve_address}") - - curve_state = self._decode_curve_state(account.data) - return curve_state - - except Exception as e: - logger.error(f"Failed to get curve state: {e!s}") - raise ValueError(f"Invalid bonding curve state: {e!s}") - - def _decode_curve_state(self, data: bytes) -> BondingCurveState: - """Decode bonding curve state from raw account data. + 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: - Decoded BondingCurveState + Dictionary with decoded bonding curve state Raises: - ValueError: If data format is invalid + ValueError: If IDL parsing fails """ - if len(data) < 8: - raise ValueError("Curve data too short") + # Use injected IDL parser to decode BondingCurve account data + decoded_curve_state = self._idl_parser.decode_account_data( + data, + "BondingCurve", + skip_discriminator=True + ) - # Check discriminator - if not data.startswith(CURVE_DISCRIMINATOR): - raise ValueError("Invalid curve discriminator") + if not decoded_curve_state: + raise ValueError("Failed to decode bonding curve state with IDL parser") - offset = 8 + # 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_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), + "token_total_supply": decoded_curve_state.get("token_total_supply", 0), + "complete": decoded_curve_state.get("complete", False), + "creator": decoded_curve_state.get("creator", ""), + } - try: - # Decode based on pump.fun BondingCurve structure: - # - virtual_token_reserves: u64 (8 bytes) - # - virtual_sol_reserves: u64 (8 bytes) - # - real_token_reserves: u64 (8 bytes) - # - real_sol_reserves: u64 (8 bytes) - # - token_total_supply: u64 (8 bytes) - # - complete: bool (1 byte) - # - padding: 7 bytes - # - creator: Pubkey (32 bytes) - - virtual_token_reserves = struct.unpack_from(" 0: + curve_data["price_per_token"] = ( + (curve_data["virtual_sol_reserves"] / curve_data["virtual_token_reserves"]) + * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL ) - - except Exception as e: - raise ValueError(f"Failed to decode curve state: {e}") + 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") + + return curve_data # Additional convenience methods for pump.fun specific operations async def calculate_expected_tokens(self, pool_address: Pubkey, sol_amount: float) -> float: @@ -321,8 +254,8 @@ class PumpFunCurveManager(CurveManager): Returns: True if curve is complete, False otherwise """ - curve_state = await self._get_curve_state_object(pool_address) - return curve_state.complete + 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. @@ -333,11 +266,11 @@ class PumpFunCurveManager(CurveManager): Returns: Dictionary with progress information """ - curve_state = await self._get_curve_state_object(pool_address) + 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 = curve_state.real_sol_reserves / LAMPORTS_PER_SOL + 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) @@ -345,10 +278,45 @@ class PumpFunCurveManager(CurveManager): progress_percentage = min((sol_raised / estimated_target_sol) * 100, 100.0) return { - "complete": curve_state.complete, + "complete": pool_state.get("complete", False), "sol_raised": sol_raised, "estimated_target_sol": estimated_target_sol, "progress_percentage": progress_percentage, - "tokens_available": curve_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 - } \ No newline at end of file + } + + 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 + """ + try: + # 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" + ] + + 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])}") + return False + + return True + + except Exception as e: + logger.error(f"Curve state validation failed: {e}") + return False \ No newline at end of file diff --git a/src/platforms/pumpfun/event_parser.py b/src/platforms/pumpfun/event_parser.py index cf26f09..ed08873 100644 --- a/src/platforms/pumpfun/event_parser.py +++ b/src/platforms/pumpfun/event_parser.py @@ -2,32 +2,43 @@ Pump.Fun implementation of EventParser interface. This module parses pump.fun-specific token creation events from various sources -by implementing the EventParser interface. +by implementing the EventParser interface with IDL-based parsing. """ import base64 import struct from time import monotonic -from typing import Any, Final +from typing import Any -import base58 from solders.pubkey import Pubkey from solders.transaction import VersionedTransaction from core.pubkeys import SystemAddresses from interfaces.core import EventParser, Platform, TokenInfo from platforms.pumpfun.address_provider import PumpFunAddresses +from utils.idl_parser import IDLParser +from utils.logger import get_logger + +logger = get_logger(__name__) class PumpFunEventParser(EventParser): - """Pump.Fun implementation of EventParser interface.""" + """Pump.Fun implementation of EventParser interface with IDL-based parsing.""" - # Discriminators for pump.fun instructions - CREATE_DISCRIMINATOR: Final[int] = 8576854823835016728 - CREATE_DISCRIMINATOR_BYTES: Final[bytes] = struct.pack(" Platform: @@ -74,7 +85,7 @@ class PumpFunEventParser(EventParser): accounts: list[int], account_keys: list[bytes] ) -> TokenInfo | None: - """Parse token creation from pump.fun instruction data. + """Parse token creation from pump.fun instruction data using injected IDL parser. Args: instruction_data: Raw instruction data @@ -84,7 +95,7 @@ class PumpFunEventParser(EventParser): Returns: TokenInfo if token creation found, None otherwise """ - if not instruction_data.startswith(self.CREATE_DISCRIMINATOR_BYTES): + if not instruction_data.startswith(self._create_discriminator_bytes): return None try: @@ -97,12 +108,14 @@ class PumpFunEventParser(EventParser): return None return Pubkey.from_bytes(account_keys[account_index]) - # Parse instruction data - token_data = self._parse_create_instruction_data(instruction_data) - if not token_data: + # 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': return None - - # Extract account information + + args = decoded.get('args', {}) + + # Extract account information based on IDL account order mint = get_account_key(0) bonding_curve = get_account_key(2) associated_bonding_curve = get_account_key(3) @@ -112,13 +125,13 @@ class PumpFunEventParser(EventParser): return None # Create creator vault - creator = Pubkey.from_string(token_data["creator"]) if token_data.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( - name=token_data["name"], - symbol=token_data["symbol"], - uri=token_data["uri"], + name=args.get("name", ""), + symbol=args.get("symbol", ""), + uri=args.get("uri", ""), mint=mint, platform=Platform.PUMP_FUN, bonding_curve=bonding_curve, @@ -129,7 +142,8 @@ class PumpFunEventParser(EventParser): creation_timestamp=monotonic(), ) - except Exception: + except Exception as e: + logger.debug(f"Failed to parse create instruction: {e}") return None def parse_token_creation_from_geyser( @@ -172,7 +186,8 @@ class PumpFunEventParser(EventParser): return None - except Exception: + except Exception as e: + logger.debug(f"Failed to parse geyser transaction: {e}") return None def get_program_id(self) -> Pubkey: @@ -189,7 +204,7 @@ class PumpFunEventParser(EventParser): Returns: List of discriminator bytes to match """ - return [self.CREATE_DISCRIMINATOR_BYTES] + return [self._create_discriminator_bytes] def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None: """Parse token creation from block data (for block listener). @@ -208,43 +223,96 @@ class PumpFunEventParser(EventParser): if not isinstance(tx, dict) or "transaction" not in tx: continue - # Decode base64 transaction data - tx_data_encoded = tx["transaction"][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] - - # 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(" 0: + try: + tx_data_encoded = tx_data[0] + tx_data_decoded = base64.b64decode(tx_data_encoded) + transaction = VersionedTransaction.from_bytes(tx_data_decoded) - if discriminator == self.CREATE_DISCRIMINATOR: - # Token creation should have substantial data and many accounts - if len(ix_data) <= 8 or len(ix.accounts) < 10: + for ix in transaction.message.instructions: + 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 - # Parse the instruction - token_info = self.parse_token_creation_from_instruction( - ix_data, ix.accounts, transaction.message.account_keys - ) - if token_info: - return token_info + # Decode instruction data + ix_data = base64.b64decode(ix["data"]) + + if len(ix_data) >= 8: + discriminator = struct.unpack(" dict | None: - """Parse the create instruction data from pump.fun. + """Parse the create instruction data from pump.fun using injected IDL parser. Args: data: Raw instruction data @@ -257,47 +325,30 @@ class PumpFunEventParser(EventParser): # Check for the correct instruction discriminator discriminator = struct.unpack(" len(data): - return None - length = struct.unpack(" len(data): - return None - value = data[offset : offset + length].decode("utf-8") - offset += length - elif field_type == "publicKey": - if offset + 32 > len(data): - return None - value = base58.b58encode(data[offset : offset + 32]).decode("utf-8") - offset += 32 - - parsed_data[field_name] = value - - return parsed_data + decoded = self._idl_parser.decode_instruction(data, dummy_account_keys, dummy_accounts) + if not decoded or decoded['instruction_name'] != 'create': + return None - except Exception: + # Extract the arguments + args = decoded.get('args', {}) + return { + "name": args.get("name", ""), + "symbol": args.get("symbol", ""), + "uri": args.get("uri", ""), + "creator": args.get("creator", ""), + } + + except Exception as e: + logger.debug(f"Failed to parse instruction data with IDL: {e}") return None def _derive_creator_vault(self, creator: Pubkey) -> Pubkey: diff --git a/src/platforms/pumpfun/instruction_builder.py b/src/platforms/pumpfun/instruction_builder.py index e495828..474dfdc 100644 --- a/src/platforms/pumpfun/instruction_builder.py +++ b/src/platforms/pumpfun/instruction_builder.py @@ -2,11 +2,10 @@ Pump.Fun implementation of InstructionBuilder interface. This module builds pump.fun-specific buy and sell instructions -by implementing the InstructionBuilder interface. +by implementing the InstructionBuilder interface with IDL-based discriminators. """ import struct -from typing import Final from solders.instruction import AccountMeta, Instruction from solders.pubkey import Pubkey @@ -14,14 +13,29 @@ from spl.token.instructions import create_idempotent_associated_token_account from core.pubkeys import TOKEN_DECIMALS, SystemAddresses from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo +from utils.idl_parser import IDLParser +from utils.logger import get_logger -# Discriminators for pump.fun instructions -BUY_DISCRIMINATOR: Final[bytes] = struct.pack(" Platform: @@ -82,7 +96,7 @@ class PumpFunInstructionBuilder(InstructionBuilder): # Build instruction data: discriminator + token_amount + max_sol_cost instruction_data = ( - BUY_DISCRIMINATOR + + self._buy_discriminator + struct.pack(" IDLParser: From 72a3d23e6bf935badb180f676d5390c736ebcdd8 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 15:12:49 +0000 Subject: [PATCH 11/30] chore: remove redundant tests --- tests/compare_listeners.py | 209 ---------------------------------- tests/test_block_listener.py | 102 ----------------- tests/test_geyser_listener.py | 109 ------------------ tests/test_logs_listener.py | 102 ----------------- 4 files changed, 522 deletions(-) delete mode 100644 tests/compare_listeners.py delete mode 100644 tests/test_block_listener.py delete mode 100644 tests/test_geyser_listener.py delete mode 100644 tests/test_logs_listener.py diff --git a/tests/compare_listeners.py b/tests/compare_listeners.py deleted file mode 100644 index 48fa8a0..0000000 --- a/tests/compare_listeners.py +++ /dev/null @@ -1,209 +0,0 @@ -""" -Test script to compare BlockListener, LogsListener, and GeyserListener -Runs all listeners simultaneously to compare their performance -""" - -import asyncio -import logging -import os -import sys -import time -from pathlib import Path - -from dotenv import load_dotenv - -sys.path.append(str(Path(__file__).parent.parent / "src")) - -from core.pubkeys import PumpAddresses -from monitoring.block_listener import BlockListener -from monitoring.geyser_listener import GeyserListener -from monitoring.logs_listener import LogsListener -from trading.base import TokenInfo - -load_dotenv() - -logging.basicConfig( - level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" -) -logger = logging.getLogger("listener-comparison") - - -class TimingTokenCallback: - def __init__(self, name: str): - self.name = name - self.detected_tokens = [] - self.detection_times = {} - - async def on_token_created(self, token_info: TokenInfo) -> None: - """Process detected token with timing information""" - token_key = str(token_info.mint) - detection_time = time.time() - - self.detected_tokens.append(token_info) - self.detection_times[token_key] = detection_time - - logger.info(f"[{self.name}] Detected: {token_info.name} ({token_info.symbol})") - print(f"\n{'=' * 50}") - print(f"[{self.name}] NEW TOKEN: {token_info.name}") - print(f"Symbol: {token_info.symbol}") - print(f"Mint: {token_info.mint}") - print(f"Detection time: {detection_time}") - print(f"{'=' * 50}\n") - - -async def listen_with_timeout(listener, callback, timeout): - """Run a listener for a specified duration""" - try: - listen_task = asyncio.create_task( - listener.listen_for_tokens(callback.on_token_created) - ) - - await asyncio.sleep(timeout) - - listen_task.cancel() - try: - await listen_task - except asyncio.CancelledError: - pass - except Exception as e: - logger.error(f"Error in listener {callback.name}: {e}") - - -async def run_comparison(test_duration: int = 300): - """Run all listeners and compare their performance""" - wss_endpoint = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") - geyser_endpoint = os.environ.get("GEYSER_ENDPOINT") - geyser_api_token = os.environ.get("GEYSER_API_TOKEN") - geyser_auth_type = os.environ.get("GEYSER_AUTH_TYPE", "x-token") - - if not wss_endpoint: - logger.error("SOLANA_NODE_WSS_ENDPOINT environment variable is not set") - return - - logger.info(f"Connecting to WebSocket: {wss_endpoint}") - - block_listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM) - logs_listener = LogsListener(wss_endpoint, PumpAddresses.PROGRAM) - - block_callback = TimingTokenCallback("BlockListener") - logs_callback = TimingTokenCallback("LogsListener") - - listener_tasks = [ - listen_with_timeout(block_listener, block_callback, test_duration), - listen_with_timeout(logs_listener, logs_callback, test_duration) - ] - - callbacks = [block_callback, logs_callback] - listener_names = ["BlockListener", "LogsListener"] - - # Initialize Geyser listener if credentials are available - if geyser_endpoint and geyser_api_token: - logger.info(f"Connecting to Geyser API: {geyser_endpoint}") - geyser_listener = GeyserListener(geyser_endpoint, geyser_api_token, geyser_auth_type, PumpAddresses.PROGRAM) - geyser_callback = TimingTokenCallback("GeyserListener") - - listener_tasks.append( - listen_with_timeout(geyser_listener, geyser_callback, test_duration) - ) - - callbacks.append(geyser_callback) - listener_names.append("GeyserListener") - else: - logger.warning("Geyser API credentials not found. Running without Geyser listener.") - - logger.info("Starting all listeners simultaneously...") - logger.info(f"Comparison running for {test_duration} seconds...") - - try: - # Start all listeners at the same time - start_time = time.time() - await asyncio.gather(*listener_tasks) - end_time = time.time() - - logger.info(f"Test completed in {end_time - start_time:.2f} seconds") - - except KeyboardInterrupt: - logger.info("Test interrupted by user") - # No need for explicit cancellation as gather() will be interrupted - - for i, callback in enumerate(callbacks): - logger.info(f"{listener_names[i]} detected {len(callback.detected_tokens)} tokens") - - # Find tokens detected by multiple listeners - all_mints = {} - for i, callback in enumerate(callbacks): - mints = {str(token.mint) for token in callback.detected_tokens} - all_mints[listener_names[i]] = mints - - # Analyze common detections between all listeners - if len(callbacks) > 1: - logger.info("\nAnalyzing token detection across listeners:") - - # Find tokens detected by all listeners - if len(callbacks) > 2: # If we have all 3 listeners - common_to_all = set.intersection(*all_mints.values()) - logger.info(f"Tokens detected by all listeners: {len(common_to_all)}") - - # Compare pairs of listeners - listeners = list(all_mints.keys()) - for i in range(len(listeners)): - for j in range(i+1, len(listeners)): - listener1 = listeners[i] - listener2 = listeners[j] - common = all_mints[listener1].intersection(all_mints[listener2]) - logger.info(f"Tokens detected by both {listener1} and {listener2}: {len(common)}") - - unique1 = all_mints[listener1] - all_mints[listener2] - unique2 = all_mints[listener2] - all_mints[listener1] - logger.info(f"Tokens unique to {listener1}: {len(unique1)}") - logger.info(f"Tokens unique to {listener2}: {len(unique2)}") - - # Find tokens detected by at least one listener - all_detected = set.union(*all_mints.values()) - logger.info(f"Total unique tokens detected by any listener: {len(all_detected)}") - - logger.info("\nDetection speed comparison:") - - # Collect all tokens detected by at least two listeners - detection_comparisons = [] - for mint in set.union(*all_mints.values()): - detections = {} - for i, callback in enumerate(callbacks): - if mint in callback.detection_times: - detections[listener_names[i]] = callback.detection_times[mint] - - if len(detections) > 1: # Only consider tokens detected by multiple listeners - detection_comparisons.append((mint, detections)) - - if detection_comparisons: - logger.info("Token | " + " | ".join(listener_names) + " | Fastest") - logger.info("-" * 80) - - for mint, detections in detection_comparisons: - # Create row with detection times or "N/A" if not detected - times = [] - for name in listener_names: - time_str = f"{detections.get(name, 0):.6f}" if name in detections else "N/A" - times.append(time_str) - - # Determine fastest listener - valid_times = {name: time for name, time in detections.items() if time > 0} - fastest = min(valid_times.items(), key=lambda x: x[1])[0] if valid_times else "N/A" - - logger.info(f"{mint[:10]}... | " + " | ".join(times) + f" | {fastest}") - else: - logger.info("No tokens were detected by multiple listeners for timing comparison") - - -if __name__ == "__main__": - test_duration = 60 # seconds - - if len(sys.argv) > 1: - try: - test_duration = int(sys.argv[1]) - except ValueError: - logger.error(f"Invalid test duration: {sys.argv[1]}. Using default of {test_duration} seconds.") - - logger.info("Starting listener comparison test") - logger.info(f"Will run for {test_duration} seconds") - asyncio.run(run_comparison(test_duration)) diff --git a/tests/test_block_listener.py b/tests/test_block_listener.py deleted file mode 100644 index cd60dd2..0000000 --- a/tests/test_block_listener.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -Test script for BlockListener -Tests websocket monitoring for new pump.fun tokens using blockSubscribe -""" - -import asyncio -import logging -import os -import sys -from pathlib import Path - -from dotenv import load_dotenv - -sys.path.append(str(Path(__file__).parent.parent / "src")) - -from core.pubkeys import PumpAddresses -from monitoring.block_listener import BlockListener -from trading.base import TokenInfo - -load_dotenv() - -logging.basicConfig( - level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" -) -logger = logging.getLogger("block-listener-test") - - -class TestTokenCallback: - def __init__(self): - self.detected_tokens = [] - - async def on_token_created(self, token_info: TokenInfo) -> None: - """Process detected token""" - logger.info(f"New token detected: {token_info.name} ({token_info.symbol})") - logger.info(f"Mint: {token_info.mint}") - self.detected_tokens.append(token_info) - print(f"\n{'=' * 50}") - print(f"NEW TOKEN: {token_info.name}") - print(f"Symbol: {token_info.symbol}") - print(f"Mint: {token_info.mint}") - print(f"URI: {token_info.uri}") - print(f"User: {token_info.user}") - print(f"Creator: {token_info.creator}") - print(f"Bonding Curve: {token_info.bonding_curve}") - print(f"Associated Bonding Curve: {token_info.associated_bonding_curve}") - print(f"{'=' * 50}\n") - - -async def test_block_listener( - match_string: str | None = None, - creator_address: str | None = None, - test_duration: int = 60, -): - """Test the block listener functionality""" - wss_endpoint = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") - if not wss_endpoint: - logger.error("SOLANA_NODE_WSS_ENDPOINT environment variable is not set") - return [] - - logger.info(f"Connecting to WebSocket: {wss_endpoint}") - listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM) - callback = TestTokenCallback() - - if match_string: - logger.info(f"Filtering tokens matching: {match_string}") - if creator_address: - logger.info(f"Filtering tokens by creator: {creator_address}") - - listen_task = asyncio.create_task( - listener.listen_for_tokens( - callback.on_token_created, - match_string=match_string, - creator_address=creator_address, - ) - ) - - logger.info(f"Listening for {test_duration} seconds...") - try: - await asyncio.sleep(test_duration) - except KeyboardInterrupt: - logger.info("Test interrupted by user") - finally: - listen_task.cancel() - try: - await listen_task - except asyncio.CancelledError: - pass - - logger.info(f"Detected {len(callback.detected_tokens)} tokens") - for token in callback.detected_tokens: - logger.info(f" - {token.name} ({token.symbol}): {token.mint}") - - return callback.detected_tokens - - -if __name__ == "__main__": - match_string = None # Update if you want to filter tokens by name/symbol - creator_address = None # Update if you want to filter tokens by creator address - test_duration = 30 - - logger.info("Starting block listener test (using blockSubscribe)") - asyncio.run(test_block_listener(match_string, creator_address, test_duration)) diff --git a/tests/test_geyser_listener.py b/tests/test_geyser_listener.py deleted file mode 100644 index 284569d..0000000 --- a/tests/test_geyser_listener.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -Test script for GeyserListener -Tests gRPC monitoring for new pump.fun tokens using Geyser -""" - -import asyncio -import logging -import os -import sys -from pathlib import Path - -from dotenv import load_dotenv - -sys.path.append(str(Path(__file__).parent.parent / "src")) - -from core.pubkeys import PumpAddresses -from monitoring.geyser_listener import GeyserListener -from trading.base import TokenInfo - -load_dotenv() - -logging.basicConfig( - level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" -) -logger = logging.getLogger("geyser-listener-test") - - -class TestTokenCallback: - def __init__(self): - self.detected_tokens = [] - - async def on_token_created(self, token_info: TokenInfo) -> None: - """Process detected token""" - logger.info(f"New token detected: {token_info.name} ({token_info.symbol})") - logger.info(f"Mint: {token_info.mint}") - self.detected_tokens.append(token_info) - print(f"\n{'=' * 50}") - print(f"NEW TOKEN: {token_info.name}") - print(f"Symbol: {token_info.symbol}") - print(f"Mint: {token_info.mint}") - print(f"URI: {token_info.uri}") - print(f"User: {token_info.user}") - print(f"Creator: {token_info.creator}") - print(f"Bonding Curve: {token_info.bonding_curve}") - print(f"Associated Bonding Curve: {token_info.associated_bonding_curve}") - print(f"{'=' * 50}\n") - - -async def test_geyser_listener( - match_string: str | None = None, - creator_address: str | None = None, - test_duration: int = 60, -): - """Test the Geyser listener functionality""" - geyser_endpoint = os.environ.get("GEYSER_ENDPOINT") - geyser_api_token = os.environ.get("GEYSER_API_TOKEN") - geyser_auth_type = os.environ.get("GEYSER_AUTH_TYPE", "x-token") - - if not geyser_endpoint: - logger.error("GEYSER_ENDPOINT environment variable is not set") - return [] - - if not geyser_api_token: - logger.error("GEYSER_API_TOKEN environment variable is not set") - return [] - - logger.info(f"Connecting to Geyser API: {geyser_endpoint}") - listener = GeyserListener(geyser_endpoint, geyser_api_token, geyser_auth_type, PumpAddresses.PROGRAM) - callback = TestTokenCallback() - - if match_string: - logger.info(f"Filtering tokens matching: {match_string}") - if creator_address: - logger.info(f"Filtering tokens by creator: {creator_address}") - - listen_task = asyncio.create_task( - listener.listen_for_tokens( - callback.on_token_created, - match_string=match_string, - creator_address=creator_address, - ) - ) - - logger.info(f"Listening for {test_duration} seconds...") - try: - await asyncio.sleep(test_duration) - except KeyboardInterrupt: - logger.info("Test interrupted by user") - finally: - listen_task.cancel() - try: - await listen_task - except asyncio.CancelledError: - pass - - logger.info(f"Detected {len(callback.detected_tokens)} tokens") - for token in callback.detected_tokens: - logger.info(f" - {token.name} ({token.symbol}): {token.mint}") - - return callback.detected_tokens - - -if __name__ == "__main__": - match_string = None # Update if you want to filter tokens by name/symbol - creator_address = None # Update if you want to filter tokens by creator address - test_duration = 30 - - logger.info("Starting Geyser listener test (using Geyser API)") - asyncio.run(test_geyser_listener(match_string, creator_address, test_duration)) diff --git a/tests/test_logs_listener.py b/tests/test_logs_listener.py deleted file mode 100644 index 90d04ed..0000000 --- a/tests/test_logs_listener.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -Test script for LogsListener -Tests websocket monitoring for new pump.fun tokens using logsSubscribe -""" - -import asyncio -import logging -import os -import sys -from pathlib import Path - -from dotenv import load_dotenv - -sys.path.append(str(Path(__file__).parent.parent / "src")) - -from core.pubkeys import PumpAddresses -from monitoring.logs_listener import LogsListener -from trading.base import TokenInfo - -load_dotenv() - -logging.basicConfig( - level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" -) -logger = logging.getLogger("logs-listener-test") - - -class TestTokenCallback: - def __init__(self): - self.detected_tokens = [] - - async def on_token_created(self, token_info: TokenInfo) -> None: - """Process detected token""" - logger.info(f"New token detected: {token_info.name} ({token_info.symbol})") - logger.info(f"Mint: {token_info.mint}") - self.detected_tokens.append(token_info) - print(f"\n{'=' * 50}") - print(f"NEW TOKEN: {token_info.name}") - print(f"Symbol: {token_info.symbol}") - print(f"Mint: {token_info.mint}") - print(f"URI: {token_info.uri}") - print(f"User: {token_info.user}") - print(f"Creator: {token_info.creator}") - print(f"Bonding Curve: {token_info.bonding_curve}") - print(f"Associated Bonding Curve: {token_info.associated_bonding_curve}") - print(f"{'=' * 50}\n") - - -async def test_logs_listener( - match_string: str | None = None, - creator_address: str | None = None, - test_duration: int = 60, -): - """Test the logs listener functionality""" - wss_endpoint = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") - if not wss_endpoint: - logger.error("SOLANA_NODE_WSS_ENDPOINT environment variable is not set") - return [] - - logger.info(f"Connecting to WebSocket: {wss_endpoint}") - listener = LogsListener(wss_endpoint, PumpAddresses.PROGRAM) - callback = TestTokenCallback() - - if match_string: - logger.info(f"Filtering tokens matching: {match_string}") - if creator_address: - logger.info(f"Filtering tokens by creator: {creator_address}") - - listen_task = asyncio.create_task( - listener.listen_for_tokens( - callback.on_token_created, - match_string=match_string, - creator_address=creator_address, - ) - ) - - logger.info(f"Listening for {test_duration} seconds...") - try: - await asyncio.sleep(test_duration) - except KeyboardInterrupt: - logger.info("Test interrupted by user") - finally: - listen_task.cancel() - try: - await listen_task - except asyncio.CancelledError: - pass - - logger.info(f"Detected {len(callback.detected_tokens)} tokens") - for token in callback.detected_tokens: - logger.info(f" - {token.name} ({token.symbol}): {token.mint}") - - return callback.detected_tokens - - -if __name__ == "__main__": - match_string = None # Update if you want to filter tokens by name/symbol - creator_address = None # Update if you want to filter tokens by creator address - test_duration = 30 - - logger.info("Starting logs listener test (using logsSubscribe)") - asyncio.run(test_logs_listener(match_string, creator_address, test_duration)) From e7107751f5aa19dd9a0d6cd4dc7250e126767d1c Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 15:38:53 +0000 Subject: [PATCH 12/30] fix(letsbonk): buy/sell instructions --- bots/bot-sniper-1-geyser.yaml | 4 +- bots/bot-sniper-2-logs.yaml | 2 +- src/platforms/letsbonk/address_provider.py | 30 +- src/platforms/letsbonk/event_parser.py | 19 +- src/platforms/letsbonk/instruction_builder.py | 93 +++--- src/platforms/pumpfun/instruction_builder.py | 287 ++++++++++++++---- trades/trades.log | 2 + 7 files changed, 306 insertions(+), 131 deletions(-) diff --git a/bots/bot-sniper-1-geyser.yaml b/bots/bot-sniper-1-geyser.yaml index ec05ef4..94812ce 100644 --- a/bots/bot-sniper-1-geyser.yaml +++ b/bots/bot-sniper-1-geyser.yaml @@ -8,11 +8,11 @@ rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}" wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" private_key: "${SOLANA_PRIVATE_KEY}" -enabled: false # You can turn off the bot w/o removing its config +enabled: true # You can turn off the bot w/o removing its config separate_process: true # Options: "pump_fun" (default), "lets_bonk" -platform: "pump_fun" +platform: "lets_bonk" # Geyser configuration (fastest method for getting updates) geyser: diff --git a/bots/bot-sniper-2-logs.yaml b/bots/bot-sniper-2-logs.yaml index 46562e5..47780b8 100644 --- a/bots/bot-sniper-2-logs.yaml +++ b/bots/bot-sniper-2-logs.yaml @@ -8,7 +8,7 @@ rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}" wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" private_key: "${SOLANA_PRIVATE_KEY}" -enabled: true # You can turn off the bot w/o removing its config +enabled: false # You can turn off the bot w/o removing its config separate_process: true # Options: "pump_fun" (default), "lets_bonk" diff --git a/src/platforms/letsbonk/address_provider.py b/src/platforms/letsbonk/address_provider.py index e9ee899..647f093 100644 --- a/src/platforms/letsbonk/address_provider.py +++ b/src/platforms/letsbonk/address_provider.py @@ -102,20 +102,16 @@ class LetsBonkAddressProvider(AddressProvider): """ accounts = {} - # Add pool state if available + # Add pool state - must be present in token_info if token_info.pool_state: accounts["pool_state"] = token_info.pool_state - # Add vault addresses if available + # Add vault addresses - must be present in token_info if token_info.base_vault: accounts["base_vault"] = token_info.base_vault if token_info.quote_vault: accounts["quote_vault"] = token_info.quote_vault - # Derive pool state if not provided - if not token_info.pool_state: - accounts["pool_state"] = self.derive_pool_address(token_info.mint) - # Derive authority PDA accounts["authority"] = self.derive_authority_pda() @@ -178,15 +174,20 @@ class LetsBonkAddressProvider(AddressProvider): """ additional_accounts = self.get_additional_accounts(token_info) + # Vault addresses must be present in token_info + if not token_info.base_vault or not token_info.quote_vault: + raise ValueError(f"Missing required vault addresses for token {token_info.mint}. " + f"base_vault: {token_info.base_vault}, quote_vault: {token_info.quote_vault}") + return { "payer": user, "authority": additional_accounts["authority"], "global_config": LetsBonkAddresses.GLOBAL_CONFIG, "platform_config": LetsBonkAddresses.PLATFORM_CONFIG, - "pool_state": additional_accounts["pool_state"], + "pool_state": token_info.pool_state, "user_base_token": self.derive_user_token_account(user, token_info.mint), - "base_vault": additional_accounts.get("base_vault", token_info.base_vault), - "quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault), + "base_vault": token_info.base_vault, + "quote_vault": token_info.quote_vault, "base_token_mint": token_info.mint, "quote_token_mint": SystemAddresses.SOL_MINT, "base_token_program": SystemAddresses.TOKEN_PROGRAM, @@ -207,15 +208,20 @@ class LetsBonkAddressProvider(AddressProvider): """ additional_accounts = self.get_additional_accounts(token_info) + # Vault addresses must be present in token_info + if not token_info.base_vault or not token_info.quote_vault: + raise ValueError(f"Missing required vault addresses for token {token_info.mint}. " + f"base_vault: {token_info.base_vault}, quote_vault: {token_info.quote_vault}") + return { "payer": user, "authority": additional_accounts["authority"], "global_config": LetsBonkAddresses.GLOBAL_CONFIG, "platform_config": LetsBonkAddresses.PLATFORM_CONFIG, - "pool_state": additional_accounts["pool_state"], + "pool_state": token_info.pool_state, "user_base_token": self.derive_user_token_account(user, token_info.mint), - "base_vault": additional_accounts.get("base_vault", token_info.base_vault), - "quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault), + "base_vault": token_info.base_vault, + "quote_vault": token_info.quote_vault, "base_token_mint": token_info.mint, "quote_token_mint": SystemAddresses.SOL_MINT, "base_token_program": SystemAddresses.TOKEN_PROGRAM, diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py index c5f6f90..21e4d0e 100644 --- a/src/platforms/letsbonk/event_parser.py +++ b/src/platforms/letsbonk/event_parser.py @@ -104,14 +104,29 @@ class LetsBonkEventParser(EventParser): if not base_mint_param: return None - # Extract account information based on IDL account order - creator = get_account_key(1) # creator account + # Extract account information based on IDL account order for initialize instruction + # From the manual example, the account order is: + # 0: creator (signer) + # 1: creator_ata (not needed for TokenInfo) + # 2: global_config + # 3: platform_config + # 4: creator + # 5: pool_state + # 6: base_mint + # 7: quote_mint (WSOL) + # 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 base_vault = get_account_key(8) # base_vault account 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}") return None return TokenInfo( diff --git a/src/platforms/letsbonk/instruction_builder.py b/src/platforms/letsbonk/instruction_builder.py index b16a6bd..8fba465 100644 --- a/src/platforms/letsbonk/instruction_builder.py +++ b/src/platforms/letsbonk/instruction_builder.py @@ -104,28 +104,28 @@ class LetsBonkInstructionBuilder(InstructionBuilder): initialize_wsol_ix = self._create_initialize_account_instruction( wsol_account, SystemAddresses.SOL_MINT, - user, - address_provider + user ) instructions.append(initialize_wsol_ix) - # 4. Build buy_exact_in instruction + # 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=accounts_info["payer"], is_signer=True, is_writable=True), - AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), - AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token - AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["quote_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=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 @@ -148,8 +148,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): close_wsol_ix = self._create_close_account_instruction( wsol_account, user, - user, - address_provider + user ) instructions.append(close_wsol_ix) @@ -204,28 +203,27 @@ class LetsBonkInstructionBuilder(InstructionBuilder): initialize_wsol_ix = self._create_initialize_account_instruction( wsol_account, SystemAddresses.SOL_MINT, - user, - address_provider + user ) instructions.append(initialize_wsol_ix) - # 3. Build sell_exact_in instruction + # 3. Build sell_exact_in instruction with correct account ordering sell_accounts = [ - AccountMeta(pubkey=accounts_info["payer"], is_signer=True, is_writable=True), - AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), - AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token - AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), - AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False), - AccountMeta(pubkey=accounts_info["quote_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=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 @@ -248,8 +246,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): close_wsol_ix = self._create_close_account_instruction( wsol_account, user, - user, - address_provider + user ) instructions.append(close_wsol_ix) @@ -278,8 +275,8 @@ class LetsBonkInstructionBuilder(InstructionBuilder): accounts_info["user_base_token"], accounts_info["base_vault"], accounts_info["quote_vault"], - accounts_info["base_token_mint"], - accounts_info["quote_token_mint"], + token_info.mint, + SystemAddresses.SOL_MINT, accounts_info["program"], ] @@ -306,8 +303,8 @@ class LetsBonkInstructionBuilder(InstructionBuilder): accounts_info["user_base_token"], accounts_info["base_vault"], accounts_info["quote_vault"], - accounts_info["base_token_mint"], - accounts_info["quote_token_mint"], + token_info.mint, + SystemAddresses.SOL_MINT, accounts_info["program"], ] @@ -328,8 +325,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): self, account: Pubkey, mint: Pubkey, - owner: Pubkey, - address_provider: AddressProvider + owner: Pubkey ) -> Instruction: """Create an InitializeAccount instruction for the Token Program. @@ -337,7 +333,6 @@ class LetsBonkInstructionBuilder(InstructionBuilder): account: The account to initialize mint: The token mint owner: The account owner - address_provider: Platform address provider Returns: Instruction for initializing the account @@ -362,8 +357,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): self, account: Pubkey, destination: Pubkey, - owner: Pubkey, - address_provider: AddressProvider + owner: Pubkey ) -> Instruction: """Create a CloseAccount instruction for the Token Program. @@ -371,7 +365,6 @@ class LetsBonkInstructionBuilder(InstructionBuilder): account: The account to close destination: Where to send the remaining lamports owner: The account owner (must sign) - address_provider: Platform address provider Returns: Instruction for closing the account diff --git a/src/platforms/pumpfun/instruction_builder.py b/src/platforms/pumpfun/instruction_builder.py index 474dfdc..8fba465 100644 --- a/src/platforms/pumpfun/instruction_builder.py +++ b/src/platforms/pumpfun/instruction_builder.py @@ -1,14 +1,17 @@ """ -Pump.Fun implementation of InstructionBuilder interface. +LetsBonk implementation of InstructionBuilder interface. -This module builds pump.fun-specific buy and sell instructions +This module builds LetsBonk (Raydium LaunchLab) specific buy and sell instructions by implementing the InstructionBuilder interface with IDL-based discriminators. """ +import hashlib import struct +import time from solders.instruction import AccountMeta, Instruction from solders.pubkey import Pubkey +from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed from spl.token.instructions import create_idempotent_associated_token_account from core.pubkeys import TOKEN_DECIMALS, SystemAddresses @@ -19,28 +22,28 @@ from utils.logger import get_logger logger = get_logger(__name__) -class PumpFunInstructionBuilder(InstructionBuilder): - """Pump.Fun implementation of InstructionBuilder interface with IDL-based discriminators.""" +class LetsBonkInstructionBuilder(InstructionBuilder): + """LetsBonk (Raydium LaunchLab) implementation of InstructionBuilder interface with IDL-based discriminators.""" def __init__(self, idl_parser: IDLParser): - """Initialize pump.fun instruction builder with injected IDL parser. + """Initialize LetsBonk instruction builder with injected IDL parser. Args: - idl_parser: Pre-loaded IDL parser for pump.fun platform + idl_parser: Pre-loaded IDL parser for LetsBonk 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"] + self._buy_exact_in_discriminator = discriminators["buy_exact_in"] + self._sell_exact_in_discriminator = discriminators["sell_exact_in"] - logger.info("Pump.Fun instruction builder initialized with injected IDL parser") + logger.info("LetsBonk instruction builder initialized with injected IDL parser") @property def platform(self) -> Platform: """Get the platform this builder serves.""" - return Platform.PUMP_FUN + return Platform.LETS_BONK async def build_buy_instruction( self, @@ -50,7 +53,7 @@ class PumpFunInstructionBuilder(InstructionBuilder): minimum_amount_out: int, address_provider: AddressProvider ) -> list[Instruction]: - """Build buy instruction(s) for pump.fun. + """Build buy instruction(s) for LetsBonk using buy_exact_in. Args: token_info: Token information @@ -67,38 +70,71 @@ class PumpFunInstructionBuilder(InstructionBuilder): # 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) + # 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. Build buy 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 = 2_039_280 # Standard account creation cost + total_lamports = amount_in + account_creation_lamports + + create_wsol_ix = create_account_with_seed( + CreateAccountWithSeedParams( + from_pubkey=user, + to_pubkey=wsol_account, + base=user, + seed=wsol_seed, + lamports=total_lamports, + space=165, # Size of a token account + 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 + ) + 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=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["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=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 + token_amount + max_sol_cost + # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate + SHARE_FEE_RATE = 0 # No sharing fee instruction_data = ( - self._buy_discriminator + - struct.pack(" list[Instruction]: - """Build sell instruction(s) for pump.fun. + """Build sell instruction(s) for LetsBonk using sell_exact_in. Args: token_info: Token information @@ -135,27 +179,60 @@ class PumpFunInstructionBuilder(InstructionBuilder): # Get all required accounts accounts_info = address_provider.get_sell_instruction_accounts(token_info, user) - # Build sell instruction accounts + # 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 = 2_039_280 + + create_wsol_ix = create_account_with_seed( + CreateAccountWithSeedParams( + from_pubkey=user, + to_pubkey=wsol_account, + base=user, + seed=wsol_seed, + lamports=account_creation_lamports, + space=165, # Size of a token account + 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 + ) + instructions.append(initialize_wsol_ix) + + # 3. Build sell_exact_in instruction with correct account ordering sell_accounts = [ - 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["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=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 + token_amount + min_sol_output + # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate + SHARE_FEE_RATE = 0 # No sharing fee instruction_data = ( - self._sell_discriminator + - struct.pack(" 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 + ) -> 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 + """ + accounts = [ + 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), + ] + + # InitializeAccount instruction discriminator (instruction 1 in Token Program) + data = bytes([1]) + + return Instruction( + program_id=SystemAddresses.TOKEN_PROGRAM, + data=data, + accounts=accounts + ) + + def _create_close_account_instruction( + 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 + """ + accounts = [ + AccountMeta(pubkey=account, is_signer=False, is_writable=True), + 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 + ) + def calculate_token_amount_raw(self, token_amount_decimal: float) -> int: """Convert decimal token amount to raw token units. diff --git a/trades/trades.log b/trades/trades.log index d226794..f1df671 100644 --- a/trades/trades.log +++ b/trades/trades.log @@ -1,2 +1,4 @@ {"timestamp": "2025-04-24T20:30:13.087092", "action": "buy", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 5e-06, "amount": 20, "tx_hash": "3JvdfCep45PUB6rCcH4dB2NuwvFP8n67SCUxqJMt4MuN5ekHYc6J27aCUfwNUK3hh5rSyKNYAWXya5vQAT2qQivB"} {"timestamp": "2025-04-24T20:30:32.759177", "action": "sell", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 3.805530050663904e-08, "amount": 20.0, "tx_hash": "5cveLfU7XhPNCPMCZfTXyugJpmAQNmi7zr81PSqs8DsP1T2swYFjJwaB5hNSf3kFPfRzgzd7QZBVaZLd5MqsJevB"} +{"timestamp": "2025-08-02T15:37:04.403139", "action": "buy", "platform": "lets_bonk", "token_address": "7o5FtYXxpX6sqtcJJ3ES4DiWt4C9HnHzKuHrZPpjbonk", "symbol": "pants", "price": 5e-06, "amount": 20, "tx_hash": "26Uu4rZ1PcnzioHwWAh3Reca4MMNZjgiQbmihjisiurpv4xcHFtvSptZoV3zUkS5bdqZ4zpLeGb46J4bWyczu6FY"} +{"timestamp": "2025-08-02T15:37:21.984944", "action": "sell", "platform": "lets_bonk", "token_address": "7o5FtYXxpX6sqtcJJ3ES4DiWt4C9HnHzKuHrZPpjbonk", "symbol": "pants", "price": 2.7959121193874663e-08, "amount": 3712.914779, "tx_hash": "5zXxjxHZuWXxGih1Aca6HoFkybiyHT4jPghhXCg3NbkyD3nuzzaJtmjg4mnoEQDeTn64b1cUZMawhmYbeq3cwjfj"} From 26c048c86a175366975fbb4b848c7a1bd42df4bf Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 16:12:45 +0000 Subject: [PATCH 13/30] fix(core): platform listener check --- bots/bot-sniper-1-geyser.yaml | 4 +- bots/bot-sniper-3-blocks.yaml | 2 +- src/config_loader.py | 2 +- src/monitoring/listener_factory.py | 4 +- src/monitoring/universal_block_listener.py | 15 +- src/monitoring/universal_geyser_listener.py | 21 +- src/monitoring/universal_logs_listener.py | 21 +- src/platforms/pumpfun/instruction_builder.py | 287 +++++-------------- 8 files changed, 115 insertions(+), 241 deletions(-) diff --git a/bots/bot-sniper-1-geyser.yaml b/bots/bot-sniper-1-geyser.yaml index 94812ce..ec05ef4 100644 --- a/bots/bot-sniper-1-geyser.yaml +++ b/bots/bot-sniper-1-geyser.yaml @@ -8,11 +8,11 @@ rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}" wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" private_key: "${SOLANA_PRIVATE_KEY}" -enabled: true # You can turn off the bot w/o removing its config +enabled: false # You can turn off the bot w/o removing its config separate_process: true # Options: "pump_fun" (default), "lets_bonk" -platform: "lets_bonk" +platform: "pump_fun" # Geyser configuration (fastest method for getting updates) geyser: diff --git a/bots/bot-sniper-3-blocks.yaml b/bots/bot-sniper-3-blocks.yaml index fe58ece..9f6bb42 100644 --- a/bots/bot-sniper-3-blocks.yaml +++ b/bots/bot-sniper-3-blocks.yaml @@ -8,7 +8,7 @@ rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}" wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" private_key: "${SOLANA_PRIVATE_KEY}" -enabled: false # You can turn off the bot w/o removing its config +enabled: true # You can turn off the bot w/o removing its config separate_process: true # Options: "pump_fun" (default), "lets_bonk" diff --git a/src/config_loader.py b/src/config_loader.py index 0a58c60..a9191e3 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -45,7 +45,7 @@ VALID_VALUES = { # Platform-specific listener compatibility PLATFORM_LISTENER_COMPATIBILITY = { Platform.PUMP_FUN: ["logs", "blocks", "geyser", "pumpportal"], - Platform.LETS_BONK: ["logs", "blocks", "geyser"], # PumpPortal is pump.fun only + Platform.LETS_BONK: ["blocks", "geyser"], # LetsBonk only supports geyser and blocks } diff --git a/src/monitoring/listener_factory.py b/src/monitoring/listener_factory.py index 93dca13..e10b5d8 100644 --- a/src/monitoring/listener_factory.py +++ b/src/monitoring/listener_factory.py @@ -132,6 +132,6 @@ class ListenerFactory: if platform == Platform.PUMP_FUN: return ["logs", "blocks", "geyser", "pumpportal"] elif platform == Platform.LETS_BONK: - return ["logs", "blocks", "geyser"] # PumpPortal is pump.fun only + return ["blocks", "geyser"] # LetsBonk only supports geyser and blocks else: - return ["logs", "blocks", "geyser"] # Default universal listeners \ No newline at end of file + return ["blocks", "geyser"] # Default universal listeners \ No newline at end of file diff --git a/src/monitoring/universal_block_listener.py b/src/monitoring/universal_block_listener.py index fc47448..3b66fbb 100644 --- a/src/monitoring/universal_block_listener.py +++ b/src/monitoring/universal_block_listener.py @@ -49,9 +49,20 @@ class UniversalBlockListener(BaseTokenListener): for platform in self.platforms: try: - # We'll need a dummy client for getting the parser + # Create a simple dummy client that doesn't start blockhash updater from core.client import SolanaClient - dummy_client = SolanaClient("http://localhost") # Won't be used for parsing + + # Create a mock client class to avoid network operations + class DummyClient(SolanaClient): + def __init__(self): + # Skip SolanaClient.__init__ to avoid starting blockhash updater + self.rpc_endpoint = "http://dummy" + self._client = None + 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 diff --git a/src/monitoring/universal_geyser_listener.py b/src/monitoring/universal_geyser_listener.py index 6703789..40dfcd0 100644 --- a/src/monitoring/universal_geyser_listener.py +++ b/src/monitoring/universal_geyser_listener.py @@ -50,13 +50,24 @@ class UniversalGeyserListener(BaseTokenListener): self.platform_parsers = {} self.platform_program_ids = set() - # Create a temporary client for getting parsers - from core.client import SolanaClient - temp_client = SolanaClient("http://temp") - for platform in self.platforms: try: - implementations = platform_factory.create_for_platform(platform, temp_client) + # 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): + # Skip SolanaClient.__init__ to avoid starting blockhash updater + self.rpc_endpoint = "http://dummy" + self._client = None + 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) parser = implementations.event_parser self.platform_parsers[platform] = parser self.platform_program_ids.add(parser.get_program_id()) diff --git a/src/monitoring/universal_logs_listener.py b/src/monitoring/universal_logs_listener.py index 5ebb32f..e76dcf9 100644 --- a/src/monitoring/universal_logs_listener.py +++ b/src/monitoring/universal_logs_listener.py @@ -45,13 +45,24 @@ class UniversalLogsListener(BaseTokenListener): self.platform_parsers = {} self.platform_program_ids = [] - # Create a temporary client for getting parsers (stateless parsers don't use it) - from core.client import SolanaClient - temp_client = SolanaClient("http://temp") - for platform in self.platforms: try: - implementations = platform_factory.create_for_platform(platform, temp_client) + # 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): + # Skip SolanaClient.__init__ to avoid starting blockhash updater + self.rpc_endpoint = "http://dummy" + self._client = None + 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) parser = implementations.event_parser self.platform_parsers[platform] = parser self.platform_program_ids.append(str(parser.get_program_id())) diff --git a/src/platforms/pumpfun/instruction_builder.py b/src/platforms/pumpfun/instruction_builder.py index 8fba465..474dfdc 100644 --- a/src/platforms/pumpfun/instruction_builder.py +++ b/src/platforms/pumpfun/instruction_builder.py @@ -1,17 +1,14 @@ """ -LetsBonk implementation of InstructionBuilder interface. +Pump.Fun implementation of InstructionBuilder interface. -This module builds LetsBonk (Raydium LaunchLab) specific buy and sell instructions +This module builds pump.fun-specific buy and sell instructions by implementing the InstructionBuilder interface with IDL-based discriminators. """ -import hashlib import struct -import time from solders.instruction import AccountMeta, Instruction from solders.pubkey import Pubkey -from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed from spl.token.instructions import create_idempotent_associated_token_account from core.pubkeys import TOKEN_DECIMALS, SystemAddresses @@ -22,28 +19,28 @@ from utils.logger import get_logger logger = get_logger(__name__) -class LetsBonkInstructionBuilder(InstructionBuilder): - """LetsBonk (Raydium LaunchLab) implementation of InstructionBuilder interface with IDL-based discriminators.""" +class PumpFunInstructionBuilder(InstructionBuilder): + """Pump.Fun implementation of InstructionBuilder interface with IDL-based discriminators.""" def __init__(self, idl_parser: IDLParser): - """Initialize LetsBonk instruction builder with injected IDL parser. + """Initialize pump.fun instruction builder with injected IDL parser. Args: - idl_parser: Pre-loaded IDL parser for LetsBonk platform + 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_exact_in_discriminator = discriminators["buy_exact_in"] - self._sell_exact_in_discriminator = discriminators["sell_exact_in"] + self._buy_discriminator = discriminators["buy"] + self._sell_discriminator = discriminators["sell"] - logger.info("LetsBonk instruction builder initialized with injected IDL parser") + logger.info("Pump.Fun instruction builder initialized with injected IDL parser") @property def platform(self) -> Platform: """Get the platform this builder serves.""" - return Platform.LETS_BONK + return Platform.PUMP_FUN async def build_buy_instruction( self, @@ -53,7 +50,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): minimum_amount_out: int, address_provider: AddressProvider ) -> list[Instruction]: - """Build buy instruction(s) for LetsBonk using buy_exact_in. + """Build buy instruction(s) for pump.fun. Args: token_info: Token information @@ -70,71 +67,38 @@ class LetsBonkInstructionBuilder(InstructionBuilder): # Get all required accounts accounts_info = address_provider.get_buy_instruction_accounts(token_info, user) - # 1. Create idempotent ATA for base token + # 1. Create idempotent ATA instruction (won't fail if ATA already exists) 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 = 2_039_280 # Standard account creation cost - total_lamports = amount_in + account_creation_lamports - - create_wsol_ix = create_account_with_seed( - CreateAccountWithSeedParams( - from_pubkey=user, - to_pubkey=wsol_account, - base=user, - seed=wsol_seed, - lamports=total_lamports, - space=165, # Size of a token account - 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 - ) - 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: + # 2. Build buy instruction 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=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["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), ] - # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate - SHARE_FEE_RATE = 0 # No sharing fee + # Build instruction data: discriminator + token_amount + max_sol_cost instruction_data = ( - self._buy_exact_in_discriminator + - struct.pack(" list[Instruction]: - """Build sell instruction(s) for LetsBonk using sell_exact_in. + """Build sell instruction(s) for pump.fun. Args: token_info: Token information @@ -179,60 +135,27 @@ class LetsBonkInstructionBuilder(InstructionBuilder): # 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 = 2_039_280 - - create_wsol_ix = create_account_with_seed( - CreateAccountWithSeedParams( - from_pubkey=user, - to_pubkey=wsol_account, - base=user, - seed=wsol_seed, - lamports=account_creation_lamports, - space=165, # Size of a token account - 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 - ) - instructions.append(initialize_wsol_ix) - - # 3. Build sell_exact_in instruction with correct account ordering + # Build sell instruction accounts 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=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["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), ] - # Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate - SHARE_FEE_RATE = 0 # No sharing fee + # Build instruction data: discriminator + token_amount + min_sol_output instruction_data = ( - self._sell_exact_in_discriminator + - struct.pack(" 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 - ) -> 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 - """ - accounts = [ - 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), - ] - - # InitializeAccount instruction discriminator (instruction 1 in Token Program) - data = bytes([1]) - - return Instruction( - program_id=SystemAddresses.TOKEN_PROGRAM, - data=data, - accounts=accounts - ) - - def _create_close_account_instruction( - 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 - """ - accounts = [ - AccountMeta(pubkey=account, is_signer=False, is_writable=True), - 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 - ) - def calculate_token_amount_raw(self, token_amount_decimal: float) -> int: """Convert decimal token amount to raw token units. From bae03c3fab26c8fc78165bba24fd3a197dc3b846 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 16:41:23 +0000 Subject: [PATCH 14/30] fix(core): block event processor account keys bytes --- bots/bot-sniper-3-blocks.yaml | 2 +- src/platforms/letsbonk/event_parser.py | 7 ++++++- src/platforms/pumpfun/event_parser.py | 7 ++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/bots/bot-sniper-3-blocks.yaml b/bots/bot-sniper-3-blocks.yaml index 9f6bb42..f3e5a4e 100644 --- a/bots/bot-sniper-3-blocks.yaml +++ b/bots/bot-sniper-3-blocks.yaml @@ -12,7 +12,7 @@ enabled: true # You can turn off the bot w/o removing its config separate_process: true # Options: "pump_fun" (default), "lets_bonk" -platform: "pump_fun" +platform: "lets_bonk" # Geyser configuration (fastest method for getting updates) geyser: diff --git a/src/platforms/letsbonk/event_parser.py b/src/platforms/letsbonk/event_parser.py index 21e4d0e..f5b4b1b 100644 --- a/src/platforms/letsbonk/event_parser.py +++ b/src/platforms/letsbonk/event_parser.py @@ -262,9 +262,14 @@ class LetsBonkEventParser(EventParser): if len(ix_data) <= 8 or len(ix.accounts) < 10: continue + account_keys_bytes = [ + bytes(key) + for key in transaction.message.account_keys + ] + # Parse the instruction token_info = self.parse_token_creation_from_instruction( - ix_data, ix.accounts, transaction.message.account_keys + ix_data, ix.accounts, account_keys_bytes ) if token_info: return token_info diff --git a/src/platforms/pumpfun/event_parser.py b/src/platforms/pumpfun/event_parser.py index ed08873..b5a7c29 100644 --- a/src/platforms/pumpfun/event_parser.py +++ b/src/platforms/pumpfun/event_parser.py @@ -248,10 +248,15 @@ class PumpFunEventParser(EventParser): # Token creation should have substantial data and many accounts if len(ix_data) <= 8 or len(ix.accounts) < 10: continue + + account_keys_bytes = [ + bytes(key) + for key in transaction.message.account_keys + ] # Parse the instruction token_info = self.parse_token_creation_from_instruction( - ix_data, ix.accounts, transaction.message.account_keys + ix_data, ix.accounts, account_keys_bytes ) if token_info: return token_info From 9b6563cecc8df84aaa7b3ba4b3d13b6838cc0474 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Sat, 2 Aug 2025 19:19:20 +0000 Subject: [PATCH 15/30] feat(core): add event idl parser --- bots/bot-sniper-2-logs.yaml | 2 +- bots/bot-sniper-3-blocks.yaml | 2 +- src/platforms/pumpfun/event_parser.py | 178 ++++++++++++++++---------- src/utils/idl_manager.py | 157 ++++++++++++++++++++++- src/utils/idl_parser.py | 120 ++++++++++++++++- 5 files changed, 385 insertions(+), 74 deletions(-) diff --git a/bots/bot-sniper-2-logs.yaml b/bots/bot-sniper-2-logs.yaml index 47780b8..46562e5 100644 --- a/bots/bot-sniper-2-logs.yaml +++ b/bots/bot-sniper-2-logs.yaml @@ -8,7 +8,7 @@ rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}" wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" private_key: "${SOLANA_PRIVATE_KEY}" -enabled: false # You can turn off the bot w/o removing its config +enabled: true # You can turn off the bot w/o removing its config separate_process: true # Options: "pump_fun" (default), "lets_bonk" diff --git a/bots/bot-sniper-3-blocks.yaml b/bots/bot-sniper-3-blocks.yaml index f3e5a4e..f2acd0f 100644 --- a/bots/bot-sniper-3-blocks.yaml +++ b/bots/bot-sniper-3-blocks.yaml @@ -8,7 +8,7 @@ rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}" wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" private_key: "${SOLANA_PRIVATE_KEY}" -enabled: true # You can turn off the bot w/o removing its config +enabled: false # You can turn off the bot w/o removing its config separate_process: true # Options: "pump_fun" (default), "lets_bonk" diff --git a/src/platforms/pumpfun/event_parser.py b/src/platforms/pumpfun/event_parser.py index b5a7c29..af5417f 100644 --- a/src/platforms/pumpfun/event_parser.py +++ b/src/platforms/pumpfun/event_parser.py @@ -2,7 +2,7 @@ Pump.Fun implementation of EventParser interface. This module parses pump.fun-specific token creation events from various sources -by implementing the EventParser interface with IDL-based parsing. +by implementing the EventParser interface with IDL-based event parsing. """ import base64 @@ -23,8 +23,8 @@ logger = get_logger(__name__) class PumpFunEventParser(EventParser): - """Pump.Fun implementation of EventParser interface with IDL-based parsing.""" - + """Pump.Fun implementation of EventParser interface with IDL-based event parsing.""" + def __init__(self, idl_parser: IDLParser): """Initialize pump.fun event parser with injected IDL parser. @@ -33,12 +33,17 @@ class PumpFunEventParser(EventParser): """ self._idl_parser = idl_parser - # Get discriminators from injected IDL parser - discriminators = self._idl_parser.get_instruction_discriminators() - self._create_discriminator_bytes = discriminators["create"] - self._create_discriminator = struct.unpack(" Platform: @@ -50,7 +55,7 @@ class PumpFunEventParser(EventParser): logs: list[str], signature: str ) -> TokenInfo | None: - """Parse token creation from pump.fun transaction logs. + """Parse token creation from pump.fun transaction logs using IDL instruction parsing. Args: logs: List of log strings from transaction @@ -59,25 +64,97 @@ class PumpFunEventParser(EventParser): Returns: TokenInfo if token creation found, None otherwise """ - # Check if this is a token creation + # Check if this is a token creation transaction if not any("Program log: Instruction: Create" in log for log in logs): return None - # Skip swaps and other operations + # Skip swaps as the first condition may pass them if any("Program log: Instruction: CreateTokenAccount" in log for log in logs): return None - # Find and process program data - for log in logs: - if "Program data:" in log: - try: - encoded_data = log.split(": ")[1] - decoded_data = base64.b64decode(encoded_data) - return self._parse_create_instruction_data(decoded_data) - except Exception: - continue - - return None + # Look for event data in the logs (CreateEvent data!) + try: + for log in logs: + if "Program data:" in log: + try: + # Extract base64 encoded event data + encoded_data = log.split("Program data: ")[1].strip() + decoded_data = base64.b64decode(encoded_data) + + # Parse as event data (CreateEvent) + # The "Program data:" in logs contains event data, not instruction data + if len(decoded_data) < 8: + continue + + # Check discriminator from program data + discriminator = decoded_data[:8] + discriminator_int = struct.unpack(" 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). @@ -244,7 +328,7 @@ class PumpFunEventParser(EventParser): if len(ix_data) >= 8: discriminator = struct.unpack("= 8: discriminator = struct.unpack(" dict | None: - """Parse the create instruction data from pump.fun using injected IDL parser. - Args: - data: Raw instruction data - - Returns: - Dictionary of parsed data or None if parsing fails - """ - if len(data) < 8: - return None - - # Check for the correct instruction discriminator - discriminator = struct.unpack(" Pubkey: """Derive the creator vault for a creator. diff --git a/src/utils/idl_manager.py b/src/utils/idl_manager.py index c5ed998..ec82af3 100644 --- a/src/utils/idl_manager.py +++ b/src/utils/idl_manager.py @@ -6,6 +6,7 @@ duplicate loading across multiple platform implementation classes. """ import os +from typing import Any from interfaces.core import Platform from utils.idl_parser import IDLParser @@ -68,7 +69,9 @@ class IDLManager: parser = IDLParser(idl_path, verbose=verbose) self._parsers[platform] = parser - logger.info(f"IDL parser loaded for {platform.value} with {len(parser.get_instruction_names())} instructions") + 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") return parser @@ -119,6 +122,10 @@ class IDLManager: 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. @@ -142,6 +149,110 @@ class IDLManager: """ 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: + """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: + """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 + """ + if not self.has_idl_support(platform): + return { + "platform": platform.value, + "has_idl_support": False, + "instructions": [], + "events": [], + "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, + "instructions": instruction_names, + "events": event_names, + "instruction_count": len(instruction_names), + "event_count": len(event_names), + } + except Exception as e: + logger.error(f"Failed to get capabilities for {platform.value}: {e}") + return { + "platform": platform.value, + "has_idl_support": False, + "error": str(e), + "instructions": [], + "events": [], + "instruction_count": 0, + "event_count": 0, + } # Global IDL manager instance @@ -192,4 +303,46 @@ def preload_platform_idl(platform: Platform, verbose: bool = False) -> None: platform: Platform to preload parser for verbose: Whether to enable verbose logging """ - get_idl_manager().preload_parser(platform, verbose) \ No newline at end of file + get_idl_manager().preload_parser(platform, verbose) + + +# -------------------------------------------------------------------------- +# 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 + """ + return get_idl_manager().get_event_discriminators(platform) + + +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: + """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 diff --git a/src/utils/idl_parser.py b/src/utils/idl_parser.py index 272f580..ffcc272 100644 --- a/src/utils/idl_parser.py +++ b/src/utils/idl_parser.py @@ -1,8 +1,9 @@ """ IDL Parser module for Solana programs. -Provides functionality to load and parse Anchor IDL files and decode instruction data. +Provides functionality to load and parse Anchor IDL files and decode instruction data and events. """ +import base64 import json import struct from typing import Any @@ -17,7 +18,7 @@ ENUM_DISCRIMINATOR_SIZE = 1 class IDLParser: - """Parser for automatically decoding instructions using IDL definitions.""" + """Parser for automatically decoding instructions and events using IDL definitions.""" # A single source of truth for primitive type information, mapping the type name # to its struct format character and size in bytes. @@ -48,14 +49,16 @@ class IDLParser: with open(idl_path) as f: self.idl = json.load(f) self.instructions: dict[bytes, dict[str, Any]] = {} + self.events: dict[bytes, dict[str, Any]] = {} self.types: dict[str, dict[str, Any]] = {} self.instruction_min_sizes: dict[bytes, int] = {} self._build_instruction_map() + self._build_event_map() self._build_type_map() self._calculate_instruction_sizes() # -------------------------------------------------------------------------- - # Public Methods (External API) + # Public Methods (External API) - Instructions # -------------------------------------------------------------------------- def get_instruction_discriminators(self) -> dict[str, bytes]: @@ -132,6 +135,108 @@ class IDLParser: 'accounts': account_info } + # -------------------------------------------------------------------------- + # Public Methods (External API) - Events + # -------------------------------------------------------------------------- + + 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()} + + def get_event_names(self) -> list[str]: + """Get a list of all available event names.""" + 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: + """ + 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. + """ + if len(event_data) < DISCRIMINATOR_SIZE: + return None + + discriminator = event_data[:DISCRIMINATOR_SIZE] + + # If event_name provided, validate it matches the discriminator + if event_name: + event_discriminators = self.get_event_discriminators() + if event_name not in event_discriminators: + if self.verbose: + print(f"Unknown event name: {event_name}") + return None + if event_discriminators[event_name] != discriminator: + if self.verbose: + print(f"Event discriminator mismatch for {event_name}") + return None + event_def = self.events[discriminator] + else: + # Try to find event by discriminator + if discriminator not in self.events: + if self.verbose: + print(f"Unknown event discriminator: {discriminator.hex()}") + return None + event_def = self.events[discriminator] + + # Decode event fields + try: + event_fields = {} + data_part = event_data[DISCRIMINATOR_SIZE:] + decode_offset = 0 + + for field in event_def.get('fields', []): + value, decode_offset = self._decode_type(data_part, decode_offset, field['type']) + event_fields[field['name']] = value + + return { + 'event_name': event_def['name'], + 'fields': event_fields + } + + except Exception as e: + if self.verbose: + print(f"❌ Error decoding event {event_def['name']}: {e}") + return 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 + """ + for log in logs: + if "Program data:" in log: + try: + # 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: """ Decode account data using a specific account type from the IDL. @@ -179,6 +284,15 @@ class IDLParser: 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', []): + # The discriminator from the JSON IDL is a list of u8 integers. + discriminator = bytes(event['discriminator']) + self.events[discriminator] = event + if self.verbose: + 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', []): From 2c6da58eb7b4b014062e9b6c8922af2b07fe3d25 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Mon, 4 Aug 2025 05:02:53 +0000 Subject: [PATCH 16/30] fix(core): fix event idl parser for logs --- src/platforms/pumpfun/event_parser.py | 195 ++++++++++++++++---------- src/utils/idl_parser.py | 82 +++++++---- 2 files changed, 180 insertions(+), 97 deletions(-) diff --git a/src/platforms/pumpfun/event_parser.py b/src/platforms/pumpfun/event_parser.py index af5417f..b3e1405 100644 --- a/src/platforms/pumpfun/event_parser.py +++ b/src/platforms/pumpfun/event_parser.py @@ -55,7 +55,7 @@ class PumpFunEventParser(EventParser): logs: list[str], signature: str ) -> TokenInfo | None: - """Parse token creation from pump.fun transaction logs using IDL instruction parsing. + """Parse token creation from pump.fun transaction logs using IDL event parsing. Args: logs: List of log strings from transaction @@ -72,88 +72,127 @@ class PumpFunEventParser(EventParser): if any("Program log: Instruction: CreateTokenAccount" in log for log in logs): 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: - for log in logs: - if "Program data:" in log: + 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: + create_instruction_found = True + logger.info(f"📝 Found Create instruction at log index {i}") + elif "Program data:" in log: + # 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)}") + + 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") + + # Try each Program data entry + 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})") + + decoded_data = base64.b64decode(encoded_data) + + if len(decoded_data) < 8: + 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(" bool: + """Check if verbose logging is enabled.""" + 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 diff --git a/src/utils/idl_parser.py b/src/utils/idl_parser.py index ffcc272..2154b9d 100644 --- a/src/utils/idl_parser.py +++ b/src/utils/idl_parser.py @@ -163,44 +163,78 @@ class IDLParser: discriminator = event_data[:DISCRIMINATOR_SIZE] - # If event_name provided, validate it matches the discriminator - if event_name: - event_discriminators = self.get_event_discriminators() - if event_name not in event_discriminators: - if self.verbose: - print(f"Unknown event name: {event_name}") - return None - if event_discriminators[event_name] != discriminator: - if self.verbose: - print(f"Event discriminator mismatch for {event_name}") - return None - event_def = self.events[discriminator] - else: - # Try to find event by discriminator - if discriminator not in self.events: - if self.verbose: - print(f"Unknown event discriminator: {discriminator.hex()}") - return None - event_def = self.events[discriminator] + # 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 self.verbose: + 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'] + 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', {}) + # Decode event fields try: event_fields = {} data_part = event_data[DISCRIMINATOR_SIZE:] decode_offset = 0 - for field in event_def.get('fields', []): - value, decode_offset = self._decode_type(data_part, decode_offset, field['type']) - event_fields[field['name']] = value + 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"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', []) + 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 + + if self.verbose: + if field['type'] == 'string': + print(f" -> '{value}'") + 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_def['name'], + 'event_name': event_name_actual, 'fields': event_fields } except Exception as e: if self.verbose: - print(f"❌ Error decoding event {event_def['name']}: {e}") + 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: From bf7fbaa8aba694821ea78762ebfc6cbf0355a39d Mon Sep 17 00:00:00 2001 From: smypmsa Date: Mon, 4 Aug 2025 05:07:05 +0000 Subject: [PATCH 17/30] chore(core): update example for logs --- bots/bot-sniper-2-logs.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bots/bot-sniper-2-logs.yaml b/bots/bot-sniper-2-logs.yaml index 46562e5..259084d 100644 --- a/bots/bot-sniper-2-logs.yaml +++ b/bots/bot-sniper-2-logs.yaml @@ -28,9 +28,9 @@ trade: sell_slippage: 0.3 # Exit strategy configuration - exit_strategy: "tp_sl" # Options: "time_based", "tp_sl", "manual" - take_profit_percentage: 0.2 # Take profit at 20% gain (0.2 = 20%) - stop_loss_percentage: 0.2 # Stop loss at 20% loss (0.2 = 20%) + exit_strategy: "time_based" # Options: "time_based", "tp_sl", "manual" + take_profit_percentage: 0.4 # Take profit at 40% gain (0.4 = 40%) + stop_loss_percentage: 0.4 # Stop loss at 40% loss (0.4 = 40%) max_hold_time: 60 # Maximum hold time in seconds price_check_interval: 2 # Check price every 2 seconds From ea1d6b469a6de8015f7750270df9fa7bf0cc6a95 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Mon, 4 Aug 2025 05:40:02 +0000 Subject: [PATCH 18/30] feat(core): pump portal fully integrated --- src/config_loader.py | 2 +- src/monitoring/listener_factory.py | 50 +++++--- ...er.py => universal_pumpportal_listener.py} | 92 ++++++++++---- src/platforms/letsbonk/__init__.py | 4 +- src/platforms/letsbonk/address_provider.py | 79 +++++++++--- .../letsbonk/pumpportal_processor.py | 118 ++++++++++++++++++ src/platforms/pumpfun/__init__.py | 6 +- .../pumpfun/pumpportal_processor.py} | 103 ++++++--------- 8 files changed, 328 insertions(+), 126 deletions(-) rename src/monitoring/{pumpportal_listener.py => universal_pumpportal_listener.py} (60%) create mode 100644 src/platforms/letsbonk/pumpportal_processor.py rename src/{monitoring/pumpportal_event_processor.py => platforms/pumpfun/pumpportal_processor.py} (53%) diff --git a/src/config_loader.py b/src/config_loader.py index a9191e3..7feb1cb 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -45,7 +45,7 @@ VALID_VALUES = { # Platform-specific listener compatibility PLATFORM_LISTENER_COMPATIBILITY = { Platform.PUMP_FUN: ["logs", "blocks", "geyser", "pumpportal"], - Platform.LETS_BONK: ["blocks", "geyser"], # LetsBonk only supports geyser and blocks + Platform.LETS_BONK: ["blocks", "geyser", "pumpportal"], } diff --git a/src/monitoring/listener_factory.py b/src/monitoring/listener_factory.py index e10b5d8..793bbc1 100644 --- a/src/monitoring/listener_factory.py +++ b/src/monitoring/listener_factory.py @@ -87,21 +87,30 @@ class ListenerFactory: return listener elif listener_type == "pumpportal": - # PumpPortal is pump.fun specific, so filter platforms - pumpfun_platforms = [Platform.PUMP_FUN] - if platforms: - pumpfun_platforms = [p for p in platforms if p == Platform.PUMP_FUN] - - if not pumpfun_platforms: - raise ValueError("PumpPortal listener only supports pump.fun platform") - - from monitoring.pumpportal_listener import PumpPortalListener - - listener = PumpPortalListener( - pump_program=None, # Will be determined from platform - pumpportal_url=pumpportal_url, + # Import the new universal PumpPortal listener + from monitoring.universal_pumpportal_listener import ( + UniversalPumpPortalListener, ) - logger.info("Created PumpPortal listener for token monitoring") + + # 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] + if unsupported: + 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] + if not filtered_platforms: + 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)]}") return listener else: @@ -132,6 +141,15 @@ class ListenerFactory: if platform == Platform.PUMP_FUN: return ["logs", "blocks", "geyser", "pumpportal"] elif platform == Platform.LETS_BONK: - return ["blocks", "geyser"] # LetsBonk only supports geyser and blocks + return ["blocks", "geyser", "pumpportal"] # Added pumpportal support else: - return ["blocks", "geyser"] # Default universal listeners \ No newline at end of file + return ["blocks", "geyser"] # Default universal listeners + + @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 diff --git a/src/monitoring/pumpportal_listener.py b/src/monitoring/universal_pumpportal_listener.py similarity index 60% rename from src/monitoring/pumpportal_listener.py rename to src/monitoring/universal_pumpportal_listener.py index 6f8ab63..a3778b5 100644 --- a/src/monitoring/pumpportal_listener.py +++ b/src/monitoring/universal_pumpportal_listener.py @@ -1,5 +1,5 @@ """ -PumpPortal monitoring for pump.fun tokens. +Universal PumpPortal listener that works with multiple platforms. """ import asyncio @@ -7,36 +7,58 @@ import json from collections.abc import Awaitable, Callable import websockets -from solders.pubkey import Pubkey -from interfaces.core import TokenInfo +from interfaces.core import Platform, TokenInfo from monitoring.base_listener import BaseTokenListener -from monitoring.pumpportal_event_processor import PumpPortalEventProcessor -from platforms.pumpfun.address_provider import PumpFunAddresses from utils.logger import get_logger logger = get_logger(__name__) -class PumpPortalListener(BaseTokenListener): - """PumpPortal listener for pump.fun token creation events.""" +class UniversalPumpPortalListener(BaseTokenListener): + """Universal PumpPortal listener that works with multiple platforms.""" def __init__( self, - pump_program: Pubkey | None = None, pumpportal_url: str = "wss://pumpportal.fun/api/data", + platforms: list[Platform] | None = None, ): - """Initialize token listener. + """Initialize universal PumpPortal listener. Args: - pump_program: Pump.fun program address (optional, will use default if None) pumpportal_url: PumpPortal WebSocket URL + platforms: List of platforms to monitor (if None, monitor all supported platforms) """ super().__init__() - self.pump_program = pump_program or PumpFunAddresses.PROGRAM self.pumpportal_url = pumpportal_url - self.event_processor = PumpPortalEventProcessor(self.pump_program) 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: + for pool_name in processor.supported_pool_names: + 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"Monitoring pools: {list(self.pool_to_processors.keys())}") async def listen_for_tokens( self, @@ -64,9 +86,10 @@ class PumpPortalListener(BaseTokenListener): continue logger.info( - f"New token detected: {token_info.name} ({token_info.symbol})" + f"New token detected: {token_info.name} ({token_info.symbol}) on {token_info.platform.value}" ) + # Apply filters if match_string and not ( match_string.lower() in token_info.name.lower() or match_string.lower() in token_info.symbol.lower() @@ -76,14 +99,14 @@ class PumpPortalListener(BaseTokenListener): ) continue - if ( - creator_address - and str(token_info.user) != creator_address - ): - logger.info( - f"Token not created by {creator_address}. Skipping..." - ) - 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 "" + if creator_address not in [creator_str, user_str]: + logger.info( + f"Token not created by {creator_address}. Skipping..." + ) + continue await token_callback(token_info) @@ -150,18 +173,35 @@ class PumpPortalListener(BaseTokenListener): data = json.loads(response) # Handle different message formats from PumpPortal - token_info = None + token_data = None if "method" in data and data["method"] == "newToken": # Standard newToken method format params = data.get("params", []) if params and len(params) > 0: token_data = params[0] - token_info = self.event_processor.process_token_data(token_data) - elif "signature" in data and "mint" in data: + elif "signature" in data and "mint" in data and "pool" in data: # Direct token data format - token_info = self.event_processor.process_token_data(data) + token_data = data - return token_info + if not token_data: + return None + + # Get pool name to determine which processor to use + pool_name = token_data.get("pool", "").lower() + if pool_name not in self.pool_to_processors: + logger.debug(f"Ignoring token from unsupported pool: {pool_name}") + return None + + # Try each processor that supports this pool + for processor in self.pool_to_processors[pool_name]: + 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") + return token_info + + logger.debug(f"No processor could handle token data from pool {pool_name}") + return None except TimeoutError: logger.debug("No data received from PumpPortal for 30 seconds") diff --git a/src/platforms/letsbonk/__init__.py b/src/platforms/letsbonk/__init__.py index 60c1cfc..110962b 100644 --- a/src/platforms/letsbonk/__init__.py +++ b/src/platforms/letsbonk/__init__.py @@ -9,11 +9,13 @@ from .address_provider import LetsBonkAddressProvider from .curve_manager import LetsBonkCurveManager from .event_parser import LetsBonkEventParser from .instruction_builder import LetsBonkInstructionBuilder +from .pumpportal_processor import LetsBonkPumpPortalProcessor # Export implementations for direct use if needed __all__ = [ 'LetsBonkAddressProvider', 'LetsBonkCurveManager', 'LetsBonkEventParser', - 'LetsBonkInstructionBuilder' + 'LetsBonkInstructionBuilder', + 'LetsBonkPumpPortalProcessor' ] \ No newline at end of file diff --git a/src/platforms/letsbonk/address_provider.py b/src/platforms/letsbonk/address_provider.py index 647f093..37d392d 100644 --- a/src/platforms/letsbonk/address_provider.py +++ b/src/platforms/letsbonk/address_provider.py @@ -79,6 +79,52 @@ class LetsBonkAddressProvider(AddressProvider): ) return pool_state + 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 + ) + return base_vault + + 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 + ) + return quote_vault + def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey: """Derive user's associated token account address. @@ -102,15 +148,22 @@ class LetsBonkAddressProvider(AddressProvider): """ accounts = {} - # Add pool state - must be present in token_info + # 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 - must be present in token_info + # 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() @@ -174,20 +227,15 @@ class LetsBonkAddressProvider(AddressProvider): """ additional_accounts = self.get_additional_accounts(token_info) - # Vault addresses must be present in token_info - if not token_info.base_vault or not token_info.quote_vault: - raise ValueError(f"Missing required vault addresses for token {token_info.mint}. " - f"base_vault: {token_info.base_vault}, quote_vault: {token_info.quote_vault}") - return { "payer": user, "authority": additional_accounts["authority"], "global_config": LetsBonkAddresses.GLOBAL_CONFIG, "platform_config": LetsBonkAddresses.PLATFORM_CONFIG, - "pool_state": token_info.pool_state, + "pool_state": additional_accounts["pool_state"], "user_base_token": self.derive_user_token_account(user, token_info.mint), - "base_vault": token_info.base_vault, - "quote_vault": token_info.quote_vault, + "base_vault": additional_accounts["base_vault"], + "quote_vault": additional_accounts["quote_vault"], "base_token_mint": token_info.mint, "quote_token_mint": SystemAddresses.SOL_MINT, "base_token_program": SystemAddresses.TOKEN_PROGRAM, @@ -208,20 +256,15 @@ class LetsBonkAddressProvider(AddressProvider): """ additional_accounts = self.get_additional_accounts(token_info) - # Vault addresses must be present in token_info - if not token_info.base_vault or not token_info.quote_vault: - raise ValueError(f"Missing required vault addresses for token {token_info.mint}. " - f"base_vault: {token_info.base_vault}, quote_vault: {token_info.quote_vault}") - return { "payer": user, "authority": additional_accounts["authority"], "global_config": LetsBonkAddresses.GLOBAL_CONFIG, "platform_config": LetsBonkAddresses.PLATFORM_CONFIG, - "pool_state": token_info.pool_state, + "pool_state": additional_accounts["pool_state"], "user_base_token": self.derive_user_token_account(user, token_info.mint), - "base_vault": token_info.base_vault, - "quote_vault": token_info.quote_vault, + "base_vault": additional_accounts["base_vault"], + "quote_vault": additional_accounts["quote_vault"], "base_token_mint": token_info.mint, "quote_token_mint": SystemAddresses.SOL_MINT, "base_token_program": SystemAddresses.TOKEN_PROGRAM, diff --git a/src/platforms/letsbonk/pumpportal_processor.py b/src/platforms/letsbonk/pumpportal_processor.py new file mode 100644 index 0000000..4af1431 --- /dev/null +++ b/src/platforms/letsbonk/pumpportal_processor.py @@ -0,0 +1,118 @@ +""" +LetsBonk-specific PumpPortal event processor. +File: src/platforms/letsbonk/pumpportal_processor.py +""" + +from solders.pubkey import Pubkey + +from interfaces.core import Platform, TokenInfo +from platforms.letsbonk.address_provider import LetsBonkAddressProvider +from utils.logger import get_logger + +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 + """ + try: + # Extract required fields for LetsBonk + name = token_data.get("name", "") + symbol = token_data.get("symbol", "") + mint_str = token_data.get("mint") + creator_str = token_data.get("traderPublicKey") + uri = token_data.get("uri", "") + + # Note: LetsBonk tokens from PumpPortal might have different field mappings + # 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") + return None + + # Convert string addresses to Pubkey objects + mint = Pubkey.from_string(mint_str) + user = Pubkey.from_string(creator_str) + creator = user + + # 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 + additional_accounts = self.address_provider.get_additional_accounts( + # Create a minimal TokenInfo to get additional accounts + TokenInfo( + name=name, + symbol=symbol, + uri=uri, + mint=mint, + platform=Platform.LETS_BONK, + pool_state=pool_state, + user=user, + creator=creator, + base_vault=None, # Will be filled from additional_accounts + 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, + # we might need to derive them or leave them None + # and let the trading logic handle the derivation + + return TokenInfo( + name=name, + symbol=symbol, + uri=uri, + mint=mint, + platform=Platform.LETS_BONK, + pool_state=pool_state, + base_vault=base_vault, + quote_vault=quote_vault, + user=user, + creator=creator, + ) + + except Exception as e: + logger.error(f"Failed to process PumpPortal LetsBonk token data: {e}") + return None \ No newline at end of file diff --git a/src/platforms/pumpfun/__init__.py b/src/platforms/pumpfun/__init__.py index bc31eb3..fffc606 100644 --- a/src/platforms/pumpfun/__init__.py +++ b/src/platforms/pumpfun/__init__.py @@ -9,11 +9,13 @@ from .address_provider import PumpFunAddressProvider from .curve_manager import PumpFunCurveManager from .event_parser import PumpFunEventParser from .instruction_builder import PumpFunInstructionBuilder +from .pumpportal_processor import PumpFunPumpPortalProcessor # Export implementations for direct use if needed __all__ = [ 'PumpFunAddressProvider', - 'PumpFunCurveManager', + 'PumpFunCurveManager', 'PumpFunEventParser', - 'PumpFunInstructionBuilder' + 'PumpFunInstructionBuilder', + 'PumpFunPumpPortalProcessor' ] \ No newline at end of file diff --git a/src/monitoring/pumpportal_event_processor.py b/src/platforms/pumpfun/pumpportal_processor.py similarity index 53% rename from src/monitoring/pumpportal_event_processor.py rename to src/platforms/pumpfun/pumpportal_processor.py index 5033e00..dc98d6b 100644 --- a/src/monitoring/pumpportal_event_processor.py +++ b/src/platforms/pumpfun/pumpportal_processor.py @@ -1,34 +1,52 @@ """ -Event processing for pump.fun tokens using PumpPortal data. +PumpFun-specific PumpPortal event processor. +File: src/platforms/pumpfun/pumpportal_processor.py """ from solders.pubkey import Pubkey -from core.pubkeys import SystemAddresses from interfaces.core import Platform, TokenInfo -from platforms.pumpfun.address_provider import PumpFunAddresses +from platforms.pumpfun.address_provider import PumpFunAddressProvider from utils.logger import get_logger logger = get_logger(__name__) -class PumpPortalEventProcessor: - """Processes token creation events from PumpPortal WebSocket.""" - - def __init__(self, pump_program: Pubkey | None = None): - """Initialize event processor. - +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: - pump_program: Pump.fun program address (optional, will use default if None) + token_data: Token data from PumpPortal + + Returns: + True if this processor can handle the token data """ - self.pump_program = pump_program or PumpFunAddresses.PROGRAM - + pool = token_data.get("pool", "").lower() + return pool in self.supported_pool_names + def process_token_data(self, token_data: dict) -> TokenInfo | None: - """Process token data from PumpPortal and extract token creation info. - + """Process pump.fun token data from PumpPortal. + Args: token_data: Token data from PumpPortal WebSocket - + Returns: TokenInfo if token creation found, None otherwise """ @@ -41,11 +59,12 @@ class PumpPortalEventProcessor: creator_str = token_data.get("traderPublicKey") # Maps to user field uri = token_data.get("uri", "") - # Additional fields available from PumpPortal but not used: - # - initialBuy: Initial buy amount in SOL - # - marketCapSol: Market cap in SOL + # Additional fields available from PumpPortal but not currently used: + # - initialBuy: Initial buy amount in tokens + # - solAmount: SOL amount spent on initial buy # - vSolInBondingCurve: Virtual SOL in bonding curve # - vTokensInBondingCurve: Virtual tokens in bonding curve + # - marketCapSol: Market cap in SOL # - signature: Transaction signature if not all([name, symbol, mint_str, bonding_curve_str, creator_str]): @@ -61,11 +80,11 @@ class PumpPortalEventProcessor: # since PumpPortal doesn't distinguish between them creator = user - # Calculate derived addresses - associated_bonding_curve = self._find_associated_bonding_curve( + # Derive additional addresses using platform provider + associated_bonding_curve = self.address_provider.derive_associated_bonding_curve( mint, bonding_curve ) - creator_vault = self._find_creator_vault(creator) + creator_vault = self.address_provider.derive_creator_vault(creator) return TokenInfo( name=name, @@ -82,44 +101,4 @@ class PumpPortalEventProcessor: except Exception as e: logger.error(f"Failed to process PumpPortal token data: {e}") - return None - - def _find_associated_bonding_curve( - self, mint: Pubkey, bonding_curve: Pubkey - ) -> Pubkey: - """ - Find the associated bonding curve for a given mint and bonding curve. - This uses the standard ATA derivation. - - Args: - mint: Token mint address - bonding_curve: Bonding curve address - - Returns: - Associated bonding curve address - """ - derived_address, _ = Pubkey.find_program_address( - [ - bytes(bonding_curve), - bytes(SystemAddresses.TOKEN_PROGRAM), - bytes(mint), - ], - SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, - ) - return derived_address - - def _find_creator_vault(self, creator: Pubkey) -> Pubkey: - """ - Find the creator vault for a creator. - - Args: - creator: Creator address - - Returns: - Creator vault address - """ - derived_address, _ = Pubkey.find_program_address( - [b"creator-vault", bytes(creator)], - PumpFunAddresses.PROGRAM, - ) - return derived_address \ No newline at end of file + return None \ No newline at end of file From 49c50857648477769d874d3e73daa5d37e70cdaf Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 5 Aug 2025 14:40:43 +0000 Subject: [PATCH 19/30] fix(core): remove silent failures --- src/config_loader.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/config_loader.py b/src/config_loader.py index 7feb1cb..cbd6ac7 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -132,8 +132,9 @@ def validate_config(config: dict) -> None: value = get_nested_value(config, path) if value not in valid_values: raise ValueError(f"{path} must be one of {valid_values}") - except ValueError: - continue + except ValueError as e: + if "Missing required config key" not in str(e): + raise # Cannot enable both dynamic and fixed priority fees try: @@ -141,8 +142,9 @@ def validate_config(config: dict) -> None: fixed = get_nested_value(config, "priority_fees.enable_fixed") if dynamic and fixed: raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously") - except ValueError: - pass + except ValueError as e: + if "Missing required config key" not in str(e): + raise # Platform-specific validation platform_str = config.get("platform", "pump_fun") @@ -176,8 +178,9 @@ def validate_platform_config(config: dict, platform: Platform) -> None: f"Listener type '{listener_type}' is not compatible with platform '{platform.value}'. " f"Compatible listeners: {compatible_listeners}" ) - except ValueError: - pass + except ValueError as e: + if "Missing required config key" not in str(e): + raise # Platform-specific configuration validation if platform == Platform.PUMP_FUN: From a831cafaf41b7dc4463218dc9973c0b732ebb767 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 5 Aug 2025 14:41:36 +0000 Subject: [PATCH 20/30] fix(core) import level --- src/config_loader.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/config_loader.py b/src/config_loader.py index cbd6ac7..1930a5a 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -2,6 +2,7 @@ Updated configuration validation with comprehensive platform support. """ +import glob import os from typing import Any @@ -287,9 +288,6 @@ def validate_all_platform_configs(config_dir: str = "bots") -> dict[str, Any]: Returns: Dictionary with validation results """ - import glob - import os - results = { "valid_configs": [], "invalid_configs": [], From 80b03748b0ee0cdf77d5dd5e71e27ba44240be0f Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 5 Aug 2025 14:44:06 +0000 Subject: [PATCH 21/30] fix(core): refactor dataclass to use class vars --- src/core/pubkeys.py | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/core/pubkeys.py b/src/core/pubkeys.py index e365ee5..0a8cd0c 100644 --- a/src/core/pubkeys.py +++ b/src/core/pubkeys.py @@ -4,7 +4,6 @@ This module contains only system-level addresses that are shared across all plat Platform-specific addresses are handled by their respective AddressProvider implementations. """ -from dataclasses import dataclass from typing import Final from solders.pubkey import Pubkey @@ -13,29 +12,35 @@ from solders.pubkey import Pubkey LAMPORTS_PER_SOL: Final[int] = 1_000_000_000 TOKEN_DECIMALS: Final[int] = 6 +# Core system programs +SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111") +TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" +) +ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( + "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" +) + +# System accounts +RENT: Final[Pubkey] = Pubkey.from_string( + "SysvarRent111111111111111111111111111111111" +) + +# Native SOL token +SOL_MINT: Final[Pubkey] = Pubkey.from_string( + "So11111111111111111111111111111111111111112" +) + -@dataclass class SystemAddresses: """System-level Solana addresses shared across all platforms.""" - - # Core system programs - SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111") - TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( - "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" - ) - ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( - "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL" - ) - # System accounts - RENT: Final[Pubkey] = Pubkey.from_string( - "SysvarRent111111111111111111111111111111111" - ) - - # Native SOL token - SOL_MINT: Final[Pubkey] = Pubkey.from_string( - "So11111111111111111111111111111111111111112" - ) + # 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]: From 3e82cfa128b7d94a46b37c8e415f7216a2b948d5 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 5 Aug 2025 14:51:48 +0000 Subject: [PATCH 22/30] fix(core): replace os with pathlib --- src/config_loader.py | 12 +++++++----- src/trading/universal_trader.py | 26 ++++++++++++++------------ src/utils/idl_manager.py | 13 ++++++------- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/config_loader.py b/src/config_loader.py index 1930a5a..26b37d5 100644 --- a/src/config_loader.py +++ b/src/config_loader.py @@ -2,8 +2,8 @@ Updated configuration validation with comprehensive platform support. """ -import glob import os +from pathlib import Path from typing import Any import yaml @@ -52,13 +52,15 @@ PLATFORM_LISTENER_COMPATIBILITY = { def load_bot_config(path: str) -> dict: """Load and validate a bot configuration from a YAML file.""" - with open(path) as f: + config_path = Path(path) + with config_path.open() as f: config = yaml.safe_load(f) env_file = config.get("env_file") if env_file: - env_path = os.path.join(os.path.dirname(path), env_file) - if os.path.exists(env_path): + config_path = Path(path) + env_path = config_path.parent / env_file + if env_path.exists(): load_dotenv(env_path, override=True) else: load_dotenv(env_file, override=True) @@ -295,7 +297,7 @@ def validate_all_platform_configs(config_dir: str = "bots") -> dict[str, Any]: "listener_distribution": {}, } - config_files = glob.glob(os.path.join(config_dir, "*.yaml")) + config_files = list(Path(config_dir).glob("*.yaml")) for config_file in config_files: try: diff --git a/src/trading/universal_trader.py b/src/trading/universal_trader.py index 40c4004..9cbe827 100644 --- a/src/trading/universal_trader.py +++ b/src/trading/universal_trader.py @@ -5,8 +5,8 @@ Cleaned up to remove all platform-specific hardcoding. import asyncio import json -import os from datetime import datetime +from pathlib import Path from time import monotonic import uvloop @@ -546,8 +546,9 @@ class UniversalTrader: async def _save_token_info(self, token_info: TokenInfo) -> None: """Save token information to a file.""" try: - os.makedirs("trades", exist_ok=True) - file_name = os.path.join("trades", f"{token_info.mint}.txt") + trades_dir = Path("trades") + trades_dir.mkdir(exist_ok=True) + file_path = trades_dir / f"{token_info.mint}.txt" # Convert to dictionary for saving - platform-agnostic token_dict = { @@ -575,17 +576,17 @@ class UniversalTrader: if field_value is not None: token_dict[field_name] = str(field_value) - with open(file_name, "w") as file: - file.write(json.dumps(token_dict, indent=2)) + file_path.write_text(json.dumps(token_dict, indent=2)) - logger.info(f"Token information saved to {file_name}") - except Exception as e: - logger.error(f"Failed to save token information: {e!s}") + logger.info(f"Token information saved to {file_path}") + 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: """Log trade information.""" try: - os.makedirs("trades", exist_ok=True) + trades_dir = Path("trades") + trades_dir.mkdir(exist_ok=True) log_entry = { "timestamp": datetime.utcnow().isoformat(), @@ -598,10 +599,11 @@ class UniversalTrader: "tx_hash": str(tx_hash) if tx_hash else None, } - with open("trades/trades.log", "a") as log_file: + log_file_path = trades_dir / "trades.log" + with log_file_path.open("a", encoding="utf-8") as log_file: log_file.write(json.dumps(log_entry) + "\n") - except Exception as e: - logger.error(f"Failed to log trade information: {e!s}") + except OSError: + logger.exception("Failed to log trade information") # Backward compatibility alias diff --git a/src/utils/idl_manager.py b/src/utils/idl_manager.py index ec82af3..576a7b2 100644 --- a/src/utils/idl_manager.py +++ b/src/utils/idl_manager.py @@ -5,7 +5,7 @@ This module provides a single point of IDL loading and management to avoid duplicate loading across multiple platform implementation classes. """ -import os +from pathlib import Path from typing import Any from interfaces.core import Platform @@ -27,14 +27,13 @@ class IDLManager: 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_dir = os.path.dirname(os.path.abspath(__file__)) - project_root = os.path.join(current_dir, "..", "..") - project_root = os.path.normpath(project_root) + current_file = Path(__file__) + project_root = current_file.parent.parent.parent # Define IDL paths for each platform self._idl_paths = { - Platform.LETS_BONK: os.path.join(project_root, "idl", "raydium_launchlab_idl.json"), - Platform.PUMP_FUN: os.path.join(project_root, "idl", "pump_fun_idl.json"), + 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: @@ -61,7 +60,7 @@ class IDLManager: idl_path = self._idl_paths[platform] # Verify IDL file exists - if not os.path.exists(idl_path): + if not idl_path.exists(): raise FileNotFoundError(f"IDL file not found for {platform.value} at {idl_path}") # Load and cache the parser From 2c2306e80b517ed6ba6a1f48f1dfa19f8ec54533 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 5 Aug 2025 14:53:07 +0000 Subject: [PATCH 23/30] fix(core): add missing method to event parser interface --- src/interfaces/core.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/interfaces/core.py b/src/interfaces/core.py index 7ad5a6d..2de0348 100644 --- a/src/interfaces/core.py +++ b/src/interfaces/core.py @@ -344,6 +344,21 @@ class EventParser(ABC): """ pass + @abstractmethod + def parse_token_creation_from_block( + 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. From 7e644286a8fbdc022f4f314887d4ba8eb9876668 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 5 Aug 2025 14:53:53 +0000 Subject: [PATCH 24/30] fix(core): remove redundant kwargs --- src/monitoring/listener_factory.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/monitoring/listener_factory.py b/src/monitoring/listener_factory.py index 793bbc1..cb3110d 100644 --- a/src/monitoring/listener_factory.py +++ b/src/monitoring/listener_factory.py @@ -21,7 +21,6 @@ class ListenerFactory: geyser_auth_type: str = "x-token", pumpportal_url: str = "wss://pumpportal.fun/api/data", platforms: list[Platform] | None = None, - **kwargs ) -> BaseTokenListener: """Create a token listener based on the specified type. @@ -33,7 +32,6 @@ class ListenerFactory: geyser_auth_type: Geyser authentication type pumpportal_url: PumpPortal WebSocket URL (for pumpportal listener) platforms: List of platforms to monitor (if None, monitor all) - **kwargs: Additional arguments Returns: Configured token listener From 53763b71231a139e91031ceb71182871ca732538 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 5 Aug 2025 14:56:59 +0000 Subject: [PATCH 25/30] fix(core): remove unused args, improve logging --- src/trading/platform_aware.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/trading/platform_aware.py b/src/trading/platform_aware.py index ba95bbc..05d63fd 100644 --- a/src/trading/platform_aware.py +++ b/src/trading/platform_aware.py @@ -41,7 +41,7 @@ class PlatformAwareBuyer(Trader): self.extreme_fast_mode = extreme_fast_mode self.extreme_fast_token_amount = extreme_fast_token_amount - async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult: + async def execute(self, token_info: TokenInfo) -> TradeResult: """Execute buy operation using platform-specific implementations.""" try: # Get platform-specific implementations @@ -123,7 +123,7 @@ class PlatformAwareBuyer(Trader): ) except Exception as e: - logger.error(f"Buy operation failed: {e!s}") + logger.exception("Buy operation failed") return TradeResult( success=False, platform=token_info.platform, @@ -162,7 +162,7 @@ class PlatformAwareSeller(Trader): self.slippage = slippage self.max_retries = max_retries - async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult: + async def execute(self, token_info: TokenInfo) -> TradeResult: """Execute sell operation using platform-specific implementations.""" try: # Get platform-specific implementations @@ -249,7 +249,7 @@ class PlatformAwareSeller(Trader): ) except Exception as e: - logger.error(f"Sell operation failed: {e!s}") + logger.exception("Sell operation failed") return TradeResult( success=False, platform=token_info.platform, From 83a7f9e4adcea6f429ca7c473aaad60726954bcf Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 5 Aug 2025 15:18:22 +0000 Subject: [PATCH 26/30] docs(readme): add letsbonk, update commands --- README.md | 32 +++++++++++-------- .../get_bonding_curve_status.py | 11 ++++--- ...ribe+abc.py => listen_logsubscribe_abc.py} | 0 3 files changed, 25 insertions(+), 18 deletions(-) rename learning-examples/listen-new-tokens/{listen_logsubscribe+abc.py => listen_logsubscribe_abc.py} (100%) diff --git a/README.md b/README.md index 5402aa4..599d8ad 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ • Start for free

-The project allows you to create bots for trading on pump.fun. Its core feature is to snipe new tokens. Besides that, learning examples contain a lot of useful scripts for different types of listeners (new tokens, migrations) and deep dive into calculations required for trading. +The project allows you to create bots for trading on pump.fun and letsbonk.fun. Its core feature is to snipe new tokens. Besides that, learning examples contain a lot of useful scripts for different types of listeners (new tokens, migrations) and deep dive into calculations required for trading. For the full walkthrough, see [Solana: Creating a trading and sniping pump.fun bot](https://docs.chainstack.com/docs/solana-creating-a-pumpfun-bot). @@ -82,7 +82,7 @@ uv pip install -e . pump_bot # Option 2: run directly -python -m src.bot_runner +uv run src/bot_runner.py ``` > **You're all set! 🎉** @@ -111,23 +111,27 @@ Also, here's a quick doc: [Listening to pump.fun migrations to Raydium](https:// ## Bonding curve state check -`check_boding_curve_status.py` — checks the state of the bonding curve associated with a token. When the bonding curve state is completed, the token is migrated to Raydium. +`get_bonding_curve_status.py` — checks the state of the bonding curve associated with a token. When the bonding curve state is completed, the token is migrated to Raydium. To run: -`python check_boding_curve_status.py TOKEN_ADDRESS` +`uv run learning-examples/bonding-curve-progress/get_bonding_curve_status.py TOKEN_ADDRESS` -## Listening to the Raydium migration +## Listening to the Pump AMM migration -When the bonding curve state completes, the liquidity and the token graduate to Raydium. +When the bonding curve state completes, the liquidity and the token graduate to Pump AMM (PumpSwap). -`listen_to_raydium_migration.py` — listens to the migration events of the tokens from pump_fun to Raydium and prints the signature of the migration, the token address, and the liquidity pool address on Raydium. +`listen_logsubscribe.py` — listens to the migration events of the tokens from bonding curves to AMM and prints the signature of the migration, the token address, and the liquidity pool address on Pump AMM. + +`listen_blocksubscribe_old_raydium.py` — listens to the migration events of the tokens from bonding curves to AMM and prints the signature of the migration, the token address, and the liquidity pool address on Pump AMM (previously, tokens migrated to Raydium). Note that it's using the [blockSubscribe]([url](https://docs.chainstack.com/reference/blocksubscribe-solana)) method that not all providers support, but Chainstack does and I (although obviously biased) found it pretty reliable. To run: -`python listen_to_raydium_migration.py` +`uv run learning-examples/listen-migrations/listen_logsubscribe.py` + +`uv run learning-examples/listen-migrations/listen_blocksubscribe_old_raydium.py` **The following two new additions are based on this question [associatedBondingCurve #26](https://github.com/chainstacklabs/pump-fun-bot/issues/26)** @@ -152,17 +156,19 @@ The following script showcase the implementation. To run: -`python compute_associated_bonding_curve.py` and then enter the token mint address. +`uv run learning-examples/compute_associated_bonding_curve.py` and then enter the token mint address. -## Listen to new direct full details +## Listen to new tokens -`listen_new_direct_full_details.py` — listens to the new direct full details events and prints the signature, the token address, the user, the bonding curve address, and the associated bonding curve address using just the `logsSubscribe` method. Basically everything you need for sniping using just `logsSubscribe` and no extra calls like doing `getTransaction` to get the missing data. It's just computed on the fly now. +`listen_logsubscribe_abc.py` — listens to new tokens and prints the signature, the token address, the user, the bonding curve address, and the associated bonding curve address using just the `logsSubscribe` method. Basically everything you need for sniping using just `logsSubscribe` (with some [limitations](https://github.com/chainstacklabs/pump-fun-bot/issues/87)) and no extra calls like doing `getTransaction` to get the missing data. It's just computed on the fly now. To run: -`python listen_new_direct_full_details.py` +`uv run learning-examples/listen-new-tokens/listen_logsubscribe_abc.py` -So now you can run `listen_create_from_blocksubscribe.py` and `listen_new_direct_full_details.py` at the same time and see which one is faster. +So now you can run `compare_listeners.py` see which one is faster. + +`uv run learning-examples/listen-new-tokens/compare_listeners.py` Also here's a doc on this: [Solana: Listening to pump.fun token mint using only logsSubscribe](https://docs.chainstack.com/docs/solana-listening-to-pumpfun-token-mint-using-only-logssubscribe) 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 88bba40..9e18a1e 100644 --- a/learning-examples/bonding-curve-progress/get_bonding_curve_status.py +++ b/learning-examples/bonding-curve-progress/get_bonding_curve_status.py @@ -6,6 +6,7 @@ Note: creator fee upgrade introduced updates in bonding curve structure. https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CREATOR_FEE_README.md """ +import argparse import asyncio import os import struct @@ -21,7 +22,7 @@ load_dotenv() RPC_ENDPOINT = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") # Change to token you want to query -TOKEN_MINT = "xWrzYY4c1LnbSkLrd2LDUg9vw7YtVyJhGmw7MABpump" +TOKEN_MINT = "..." # Constants PUMP_PROGRAM_ID: Final[Pubkey] = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P") @@ -172,11 +173,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", help="The token mint address" - #args = parser.parse_args() + parser = argparse.ArgumentParser(description="Check token bonding curve status") + parser.add_argument("mint_address", nargs='?', help="The token mint address", default=TOKEN_MINT) + args = parser.parse_args() - asyncio.run(check_token_status(TOKEN_MINT)) + asyncio.run(check_token_status(args.mint_address)) if __name__ == "__main__": diff --git a/learning-examples/listen-new-tokens/listen_logsubscribe+abc.py b/learning-examples/listen-new-tokens/listen_logsubscribe_abc.py similarity index 100% rename from learning-examples/listen-new-tokens/listen_logsubscribe+abc.py rename to learning-examples/listen-new-tokens/listen_logsubscribe_abc.py From 874d148777d8068dc59883ae8a38ab36e7585dec Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 5 Aug 2025 16:09:05 +0000 Subject: [PATCH 27/30] fix(core): use logger exception --- src/core/client.py | 14 +++++----- src/core/priority_fee/dynamic_fee.py | 6 ++-- src/monitoring/universal_block_listener.py | 12 ++++---- src/monitoring/universal_geyser_listener.py | 13 ++++----- src/monitoring/universal_logs_listener.py | 12 ++++---- .../universal_pumpportal_listener.py | 12 ++++---- src/platforms/letsbonk/curve_manager.py | 6 ++-- .../letsbonk/pumpportal_processor.py | 4 +-- src/platforms/pumpfun/curve_manager.py | 6 ++-- src/platforms/pumpfun/event_parser.py | 4 +-- src/platforms/pumpfun/pumpportal_processor.py | 4 +-- src/trading/universal_trader.py | 28 +++++++++---------- src/utils/idl_manager.py | 2 +- 13 files changed, 60 insertions(+), 63 deletions(-) diff --git a/src/core/client.py b/src/core/client.py index 1bda0ab..08febce 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -186,7 +186,7 @@ class SolanaClient: except Exception as e: if attempt == max_retries - 1: - logger.error( + logger.exception( f"Failed to send transaction after {max_retries} attempts" ) raise @@ -215,8 +215,8 @@ class SolanaClient: signature, commitment=commitment, sleep_seconds=1 ) return True - except Exception as e: - logger.error(f"Failed to confirm transaction {signature}: {e!s}") + except Exception: + logger.exception(f"Failed to confirm transaction {signature}") return False async def post_rpc(self, body: dict[str, Any]) -> dict[str, Any] | None: @@ -238,9 +238,9 @@ class SolanaClient: ) as response: response.raise_for_status() return await response.json() - except aiohttp.ClientError as e: - logger.error(f"RPC request failed: {e!s}", exc_info=True) + except aiohttp.ClientError: + logger.exception("RPC request failed") return None - except json.JSONDecodeError as e: - logger.error(f"Failed to decode RPC response: {e!s}", exc_info=True) + except json.JSONDecodeError: + logger.exception("Failed to decode RPC response") return None diff --git a/src/core/priority_fee/dynamic_fee.py b/src/core/priority_fee/dynamic_fee.py index b04235d..aaf579c 100644 --- a/src/core/priority_fee/dynamic_fee.py +++ b/src/core/priority_fee/dynamic_fee.py @@ -62,8 +62,6 @@ class DynamicPriorityFee(PriorityFeePlugin): return prior_fee - except Exception as e: - logger.error( - f"Failed to fetch recent priority fee: {str(e)}", exc_info=True - ) + except Exception: + logger.exception("Failed to fetch recent priority fee") return None diff --git a/src/monitoring/universal_block_listener.py b/src/monitoring/universal_block_listener.py index 3b66fbb..1daa42e 100644 --- a/src/monitoring/universal_block_listener.py +++ b/src/monitoring/universal_block_listener.py @@ -129,8 +129,8 @@ class UniversalBlockListener(BaseTokenListener): logger.warning("WebSocket connection closed. Reconnecting...") ping_task.cancel() - except Exception as e: - logger.error(f"WebSocket connection error: {e!s}") + except Exception: + logger.exception("WebSocket connection error") logger.info("Reconnecting in 5 seconds...") await asyncio.sleep(5) @@ -183,8 +183,8 @@ class UniversalBlockListener(BaseTokenListener): return except asyncio.CancelledError: pass - except Exception as e: - logger.error(f"Ping error: {e!s}") + except Exception: + logger.exception("Ping error") async def _wait_for_token_creation(self, websocket) -> TokenInfo | None: """Wait for token creation event from any platform. @@ -234,7 +234,7 @@ class UniversalBlockListener(BaseTokenListener): except websockets.exceptions.ConnectionClosed: logger.warning("WebSocket connection closed") raise - except Exception as e: - logger.error(f"Error processing WebSocket message: {e!s}") + except Exception: + logger.exception("Error processing WebSocket message") return None \ No newline at end of file diff --git a/src/monitoring/universal_geyser_listener.py b/src/monitoring/universal_geyser_listener.py index 40dfcd0..84de51a 100644 --- a/src/monitoring/universal_geyser_listener.py +++ b/src/monitoring/universal_geyser_listener.py @@ -160,18 +160,17 @@ class UniversalGeyserListener(BaseTokenListener): await token_callback(token_info) except Exception as e: - import grpc if isinstance(e, grpc.aio.AioRpcError): - logger.error(f"gRPC error: {e.details()}") + logger.exception(f"gRPC error: {e.details()}") else: - logger.error(f"Geyser error: {e}") + logger.exception("Geyser error occurred") await asyncio.sleep(5) finally: await channel.close() - except Exception as e: - logger.error(f"Geyser connection error: {e}") + except Exception: + logger.exception("Geyser connection error") logger.info("Reconnecting in 10 seconds...") await asyncio.sleep(10) @@ -208,6 +207,6 @@ class UniversalGeyserListener(BaseTokenListener): return None - except Exception as e: - logger.error(f"Error processing Geyser update: {e}") + except Exception: + logger.exception("Error processing Geyser update") return None \ No newline at end of file diff --git a/src/monitoring/universal_logs_listener.py b/src/monitoring/universal_logs_listener.py index e76dcf9..b6fe49c 100644 --- a/src/monitoring/universal_logs_listener.py +++ b/src/monitoring/universal_logs_listener.py @@ -127,8 +127,8 @@ class UniversalLogsListener(BaseTokenListener): logger.warning("WebSocket connection closed. Reconnecting...") ping_task.cancel() - except Exception as e: - logger.error(f"WebSocket connection error: {e!s}") + except Exception: + logger.exception("WebSocket connection error") logger.info("Reconnecting in 5 seconds...") await asyncio.sleep(5) @@ -177,8 +177,8 @@ class UniversalLogsListener(BaseTokenListener): return except asyncio.CancelledError: pass - except Exception as e: - logger.error(f"Ping error: {e!s}") + except Exception: + logger.exception("Ping error") async def _wait_for_token_creation(self, websocket) -> TokenInfo | None: """Wait for token creation events from any platform.""" @@ -206,7 +206,7 @@ class UniversalLogsListener(BaseTokenListener): except websockets.exceptions.ConnectionClosed: logger.warning("WebSocket connection closed") raise - except Exception as e: - logger.error(f"Error processing WebSocket message: {e!s}") + except Exception: + logger.exception("Error processing WebSocket message") return None \ No newline at end of file diff --git a/src/monitoring/universal_pumpportal_listener.py b/src/monitoring/universal_pumpportal_listener.py index a3778b5..5a4818e 100644 --- a/src/monitoring/universal_pumpportal_listener.py +++ b/src/monitoring/universal_pumpportal_listener.py @@ -156,8 +156,8 @@ class UniversalPumpPortalListener(BaseTokenListener): return except asyncio.CancelledError: pass - except Exception as e: - logger.error(f"Ping error: {e}") + except Exception: + logger.exception("Ping error") async def _wait_for_token_creation(self, websocket) -> TokenInfo | None: """Wait for token creation event from PumpPortal. @@ -208,9 +208,9 @@ class UniversalPumpPortalListener(BaseTokenListener): except websockets.exceptions.ConnectionClosed: logger.warning("PumpPortal WebSocket connection closed") raise - except json.JSONDecodeError as e: - logger.error(f"Failed to decode PumpPortal message: {e}") - except Exception as e: - logger.error(f"Error processing PumpPortal WebSocket message: {e}") + except json.JSONDecodeError: + logger.exception("Failed to decode PumpPortal message") + except Exception: + logger.exception("Error processing PumpPortal WebSocket message") return None \ No newline at end of file diff --git a/src/platforms/letsbonk/curve_manager.py b/src/platforms/letsbonk/curve_manager.py index 395fe6e..2b46594 100644 --- a/src/platforms/letsbonk/curve_manager.py +++ b/src/platforms/letsbonk/curve_manager.py @@ -60,7 +60,7 @@ class LetsBonkCurveManager(CurveManager): return pool_state_data except Exception as e: - logger.error(f"Failed to get pool state: {e!s}") + logger.exception("Failed to get pool state") raise ValueError(f"Invalid pool state: {e!s}") async def calculate_price(self, pool_address: Pubkey) -> float: @@ -239,6 +239,6 @@ class LetsBonkCurveManager(CurveManager): return True - except Exception as e: - logger.error(f"Pool state validation failed: {e}") + except Exception: + logger.exception("Pool state validation failed") return False \ No newline at end of file diff --git a/src/platforms/letsbonk/pumpportal_processor.py b/src/platforms/letsbonk/pumpportal_processor.py index 4af1431..ce03b08 100644 --- a/src/platforms/letsbonk/pumpportal_processor.py +++ b/src/platforms/letsbonk/pumpportal_processor.py @@ -113,6 +113,6 @@ class LetsBonkPumpPortalProcessor: creator=creator, ) - except Exception as e: - logger.error(f"Failed to process PumpPortal LetsBonk token data: {e}") + except Exception: + logger.exception("Failed to process PumpPortal LetsBonk token data") return None \ No newline at end of file diff --git a/src/platforms/pumpfun/curve_manager.py b/src/platforms/pumpfun/curve_manager.py index b113c3c..b4948e6 100644 --- a/src/platforms/pumpfun/curve_manager.py +++ b/src/platforms/pumpfun/curve_manager.py @@ -58,7 +58,7 @@ class PumpFunCurveManager(CurveManager): return curve_state_data except Exception as e: - logger.error(f"Failed to get curve state: {e!s}") + 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: @@ -317,6 +317,6 @@ class PumpFunCurveManager(CurveManager): return True - except Exception as e: - logger.error(f"Curve state validation failed: {e}") + except Exception: + logger.exception("Curve state validation failed") return False \ No newline at end of file diff --git a/src/platforms/pumpfun/event_parser.py b/src/platforms/pumpfun/event_parser.py index b3e1405..3ff620d 100644 --- a/src/platforms/pumpfun/event_parser.py +++ b/src/platforms/pumpfun/event_parser.py @@ -191,8 +191,8 @@ class PumpFunEventParser(EventParser): logger.info("❌ No valid CreateEvent found in any Program data entries") return None - except Exception as e: - logger.error(f"Failed to parse token creation from logs: {e}") + except Exception: + logger.exception("Failed to parse token creation from logs") return None def parse_token_creation_from_instruction( diff --git a/src/platforms/pumpfun/pumpportal_processor.py b/src/platforms/pumpfun/pumpportal_processor.py index dc98d6b..1c56123 100644 --- a/src/platforms/pumpfun/pumpportal_processor.py +++ b/src/platforms/pumpfun/pumpportal_processor.py @@ -99,6 +99,6 @@ class PumpFunPumpPortalProcessor: creator_vault=creator_vault, ) - except Exception as e: - logger.error(f"Failed to process PumpPortal token data: {e}") + except Exception: + logger.exception("Failed to process PumpPortal token data") return None \ No newline at end of file diff --git a/src/trading/universal_trader.py b/src/trading/universal_trader.py index 9cbe827..3b50da6 100644 --- a/src/trading/universal_trader.py +++ b/src/trading/universal_trader.py @@ -110,8 +110,8 @@ class UniversalTrader: 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 as e: - logger.error(f"Platform validation failed: {e}") + except Exception: + logger.exception("Platform validation failed") raise # Get platform-specific implementations @@ -234,8 +234,8 @@ class UniversalTrader: self.match_string, self.bro_address, ) - except Exception as e: - logger.error(f"Token listening stopped due to error: {e!s}") + except Exception: + logger.exception("Token listening stopped due to error") finally: processor_task.cancel() try: @@ -243,8 +243,8 @@ class UniversalTrader: except asyncio.CancelledError: pass - except Exception as e: - logger.error(f"Trading stopped due to error: {e!s}") + except Exception: + logger.exception("Trading stopped due to error") finally: await self._cleanup_resources() @@ -306,8 +306,8 @@ class UniversalTrader: self.cleanup_with_priority_fee, self.cleanup_force_close_with_burn, ) - except Exception as e: - logger.error(f"Error during cleanup: {e!s}") + except Exception: + logger.exception("Error during cleanup") old_keys = {k for k in self.token_timestamps if k not in self.processed_tokens} for key in old_keys: @@ -352,8 +352,8 @@ class UniversalTrader: except asyncio.CancelledError: logger.info("Token queue processor was cancelled") break - except Exception as e: - logger.error(f"Error in token queue processor: {e!s}") + except Exception: + logger.exception("Error in token queue processor") finally: self.token_queue.task_done() @@ -385,8 +385,8 @@ class UniversalTrader: logger.info(f"YOLO mode enabled. Waiting {self.wait_time_before_new_token} seconds before looking for next token...") await asyncio.sleep(self.wait_time_before_new_token) - except Exception as e: - logger.error(f"Error handling token {token_info.symbol}: {e!s}") + except Exception: + logger.exception(f"Error handling token {token_info.symbol}") async def _handle_successful_buy(self, token_info: TokenInfo, buy_result: TradeResult) -> None: """Handle successful token purchase.""" @@ -526,8 +526,8 @@ class UniversalTrader: # Wait before next price check await asyncio.sleep(self.price_check_interval) - except Exception as e: - logger.error(f"Error monitoring position: {e}") + except Exception: + logger.exception("Error monitoring position") await asyncio.sleep(self.price_check_interval) # Continue monitoring despite errors def _get_pool_address(self, token_info: TokenInfo) -> Pubkey: diff --git a/src/utils/idl_manager.py b/src/utils/idl_manager.py index 576a7b2..e8fb69a 100644 --- a/src/utils/idl_manager.py +++ b/src/utils/idl_manager.py @@ -242,7 +242,7 @@ class IDLManager: "event_count": len(event_names), } except Exception as e: - logger.error(f"Failed to get capabilities for {platform.value}: {e}") + logger.exception(f"Failed to get capabilities for {platform.value}") return { "platform": platform.value, "has_idl_support": False, From 329266611cb4fdab70d023644a2262baa911942d Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 5 Aug 2025 20:19:00 +0000 Subject: [PATCH 28/30] fix(core): minor fixes in hardcoded values --- src/core/pubkeys.py | 4 ++++ src/monitoring/universal_geyser_listener.py | 4 +--- src/platforms/letsbonk/curve_manager.py | 4 ++-- src/platforms/letsbonk/instruction_builder.py | 15 ++++++++++----- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/core/pubkeys.py b/src/core/pubkeys.py index 0a8cd0c..586975d 100644 --- a/src/core/pubkeys.py +++ b/src/core/pubkeys.py @@ -12,6 +12,10 @@ from solders.pubkey import Pubkey LAMPORTS_PER_SOL: Final[int] = 1_000_000_000 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 + # Core system programs SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111") TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string( diff --git a/src/monitoring/universal_geyser_listener.py b/src/monitoring/universal_geyser_listener.py index 84de51a..cf2933d 100644 --- a/src/monitoring/universal_geyser_listener.py +++ b/src/monitoring/universal_geyser_listener.py @@ -10,6 +10,7 @@ import grpc from geyser.generated import geyser_pb2, geyser_pb2_grpc from interfaces.core import Platform, TokenInfo from monitoring.base_listener import BaseTokenListener +from platforms import platform_factory from utils.logger import get_logger logger = get_logger(__name__) @@ -38,9 +39,6 @@ class UniversalGeyserListener(BaseTokenListener): f"Expected one of {valid_auth_types}" ) - # Import platform factory and get supported platforms - from platforms import platform_factory - if platforms is None: self.platforms = platform_factory.get_supported_platforms() else: diff --git a/src/platforms/letsbonk/curve_manager.py b/src/platforms/letsbonk/curve_manager.py index 2b46594..4f49c3f 100644 --- a/src/platforms/letsbonk/curve_manager.py +++ b/src/platforms/letsbonk/curve_manager.py @@ -209,7 +209,7 @@ class LetsBonkCurveManager(CurveManager): return pool_data - def validate_pool_state_structure(self, pool_address: Pubkey) -> bool: + async def validate_pool_state_structure(self, pool_address: Pubkey) -> bool: """Validate that the pool state structure matches IDL expectations. Args: @@ -221,7 +221,7 @@ class LetsBonkCurveManager(CurveManager): try: # This would be used during development/testing to ensure # the IDL parsing is working correctly - pool_state = self.get_pool_state(pool_address) + pool_state = await self.get_pool_state(pool_address) required_fields = [ "virtual_base", "virtual_quote", diff --git a/src/platforms/letsbonk/instruction_builder.py b/src/platforms/letsbonk/instruction_builder.py index 8fba465..0a4857e 100644 --- a/src/platforms/letsbonk/instruction_builder.py +++ b/src/platforms/letsbonk/instruction_builder.py @@ -14,7 +14,12 @@ from solders.pubkey import Pubkey from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed from spl.token.instructions import create_idempotent_associated_token_account -from core.pubkeys import TOKEN_DECIMALS, SystemAddresses +from core.pubkeys import ( + TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE, + TOKEN_ACCOUNT_SIZE, + TOKEN_DECIMALS, + SystemAddresses, +) from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo from utils.idl_parser import IDLParser from utils.logger import get_logger @@ -84,7 +89,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed) # Account creation cost + amount to spend - account_creation_lamports = 2_039_280 # Standard account creation cost + account_creation_lamports = TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE total_lamports = amount_in + account_creation_lamports create_wsol_ix = create_account_with_seed( @@ -94,7 +99,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): base=user, seed=wsol_seed, lamports=total_lamports, - space=165, # Size of a token account + space=TOKEN_ACCOUNT_SIZE, owner=SystemAddresses.TOKEN_PROGRAM ) ) @@ -184,7 +189,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed) # Minimal account creation cost - account_creation_lamports = 2_039_280 + account_creation_lamports = TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE create_wsol_ix = create_account_with_seed( CreateAccountWithSeedParams( @@ -193,7 +198,7 @@ class LetsBonkInstructionBuilder(InstructionBuilder): base=user, seed=wsol_seed, lamports=account_creation_lamports, - space=165, # Size of a token account + space=TOKEN_ACCOUNT_SIZE, owner=SystemAddresses.TOKEN_PROGRAM ) ) From a9c58faf9ec4abc9741679dc587459f1319e809a Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 5 Aug 2025 20:30:41 +0000 Subject: [PATCH 29/30] fix(pump): derive associated bonding curve --- src/platforms/pumpfun/address_provider.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/platforms/pumpfun/address_provider.py b/src/platforms/pumpfun/address_provider.py index 9212a4e..4f6d4ba 100644 --- a/src/platforms/pumpfun/address_provider.py +++ b/src/platforms/pumpfun/address_provider.py @@ -177,7 +177,15 @@ class PumpFunAddressProvider(AddressProvider): Returns: Associated bonding curve address """ - return get_associated_token_address(bonding_curve, mint) + derived_address, _ = Pubkey.find_program_address( + [ + bytes(bonding_curve), + bytes(SystemAddresses.TOKEN_PROGRAM), + bytes(mint), + ], + SystemAddresses.ASSOCIATED_TOKEN_PROGRAM, + ) + return derived_address def derive_creator_vault(self, creator: Pubkey) -> Pubkey: """Derive the creator vault address. From cb5c9ed1cd05f7f51f14dbfe322f0280be2afc13 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Tue, 5 Aug 2025 20:48:03 +0000 Subject: [PATCH 30/30] chore: pumpportal time based config example --- bots/bot-sniper-4-pp.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bots/bot-sniper-4-pp.yaml b/bots/bot-sniper-4-pp.yaml index 4e8df34..4646d35 100644 --- a/bots/bot-sniper-4-pp.yaml +++ b/bots/bot-sniper-4-pp.yaml @@ -12,7 +12,7 @@ enabled: false # You can turn off the bot w/o removing its config separate_process: true # Options: "pump_fun" (default), "lets_bonk" -platform: "pump_fun" +platform: "lets_bonk" # PumpPortal configuration (optional - uses default URL if not specified) pumpportal: @@ -26,7 +26,7 @@ trade: sell_slippage: 0.3 # Exit strategy configuration - exit_strategy: "tp_sl" # Options: "time_based", "tp_sl", "manual" + exit_strategy: "time_based" # Options: "time_based", "tp_sl", "manual" take_profit_percentage: 0.1 # Take profit at 10% gain (0.1 = 10%) stop_loss_percentage: 0.1 # Stop loss at 10% loss (0.1 = 10%) max_hold_time: 600 # Maximum hold time in seconds (600 = 10 minutes)