Merge pull request #127 from chainstacklabs/feat/platform-abstraction

Refactor platform support, add letsbonk
This commit is contained in:
Ake
2025-08-06 17:06:33 +08:00
committed by GitHub
62 changed files with 10773 additions and 3997 deletions
-130
View File
@@ -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
-68
View File
@@ -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
-76
View File
@@ -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
-89
View File
@@ -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
-96
View File
@@ -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
-89
View File
@@ -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.
-115
View File
@@ -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
-153
View File
@@ -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
-92
View File
@@ -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
-44
View File
@@ -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
-120
View File
@@ -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`
+19 -13
View File
@@ -12,7 +12,7 @@
• <a target="_blank" href="https://console.chainstack.com/user/account/create">Start for free</a> •
</p>
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)
+3
View File
@@ -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}"
+6 -3
View File
@@ -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}"
@@ -25,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
+3
View File
@@ -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: "lets_bonk"
# Geyser configuration (fastest method for getting updates)
geyser:
endpoint: "${GEYSER_ENDPOINT}"
+4 -1
View File
@@ -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: "lets_bonk"
# PumpPortal configuration (optional - uses default URL if not specified)
pumpportal:
url: "wss://pumpportal.fun/api/data" # Default PumpPortal WebSocket URL
@@ -23,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)
File diff suppressed because it is too large Load Diff
@@ -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__":
+165 -85
View File
@@ -8,18 +8,18 @@ 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,
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)
@@ -30,79 +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)
trader = PumpTrader(
# Connection settings
rpc_endpoint=cfg["rpc_endpoint"],
wss_endpoint=cfg["wss_endpoint"],
private_key=cfg["private_key"],
# Trade parameters
buy_amount=cfg["trade"]["buy_amount"],
buy_slippage=cfg["trade"]["buy_slippage"],
sell_slippage=cfg["trade"]["sell_slippage"],
# Extreme fast mode settings
extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False),
extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30),
# 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"),
# 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("timing", {}).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):
@@ -110,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")
@@ -141,23 +177,52 @@ def run_all_bots():
skipped_bots += 1
continue
# 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}'. 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}' 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}")
skipped_bots += 1
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"
)
# Wait for all processes to complete
for p in processes:
p.join()
logging.info(f"Process {p.name} completed")
@@ -169,8 +234,23 @@ def main() -> None:
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
# 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}")
run_all_bots()
if __name__ == "__main__":
main()
main()
+226 -113
View File
@@ -1,9 +1,17 @@
"""
Updated configuration validation with comprehensive platform support.
"""
import os
from pathlib import Path
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",
@@ -17,51 +25,14 @@ 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
@@ -69,49 +40,43 @@ 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-specific listener compatibility
PLATFORM_LISTENER_COMPATIBILITY = {
Platform.PUMP_FUN: ["logs", "blocks", "geyser", "pumpportal"],
Platform.LETS_BONK: ["blocks", "geyser", "pumpportal"],
}
def load_bot_config(path: str) -> dict:
"""
Load and validate a bot configuration from a YAML file.
Args:
path: Path to the YAML configuration file (relative or absolute)
Returns:
Validated configuration dictionary
Raises:
FileNotFoundError: If the configuration file doesn't exist
ValueError: If the configuration is invalid
"""
with open(path) as f:
"""Load and validate a bot configuration from a YAML file."""
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:
# If not found relative to config, try relative to current working directory
load_dotenv(env_file, override=True)
resolve_env_vars(config)
# Set default platform if not specified (backward compatibility)
if "platform" not in config:
config["platform"] = "pump_fun"
validate_config(config)
return config
def resolve_env_vars(config: dict) -> None:
"""
Recursively resolve environment variables in the configuration.
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]
@@ -132,19 +97,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:
@@ -155,18 +108,12 @@ def get_nested_value(config: dict, path: str) -> Any:
def validate_config(config: dict) -> None:
"""
Validate the configuration against defined rules.
Args:
config: Configuration dictionary to validate
Raises:
ValueError: If the configuration is invalid
"""
"""Validate the configuration against defined rules with platform support."""
# 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)
@@ -178,10 +125,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
@@ -190,55 +135,223 @@ 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:
# Skip if the field is missing
continue
except ValueError as e:
if "Missing required config key" not in str(e):
raise
# Cannot enable both dynamic and fixed priority fees
try:
dynamic = get_nested_value(config, "priority_fees.enable_dynamic")
fixed = get_nested_value(config, "priority_fees.enable_fixed")
if dynamic and fixed:
raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously")
except ValueError as e:
if "Missing required config key" not in str(e):
raise
# 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."""
# 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
# 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(
"Cannot enable both dynamic and fixed priority fees simultaneously"
f"Listener type '{listener_type}' is not compatible with platform '{platform.value}'. "
f"Compatible listeners: {compatible_listeners}"
)
except ValueError:
# Skip if one of the fields is missing
except ValueError as e:
if "Missing required config key" not in str(e):
raise
# Platform-specific configuration validation
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 print_config_summary(config: dict) -> None:
"""
Print a summary of the loaded configuration.
def get_platform_from_config(config: dict) -> Platform:
"""Extract platform enum from configuration."""
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 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"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}"
)
print(f"Platform: {platform_str}")
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!")
def validate_all_platform_configs(config_dir: str = "bots") -> dict[str, Any]:
"""Validate all bot configurations in a directory.
Args:
config_dir: Directory containing bot config files
Returns:
Dictionary with validation results
"""
results = {
"valid_configs": [],
"invalid_configs": [],
"platform_distribution": {},
"listener_distribution": {},
}
config_files = list(Path(config_dir).glob("*.yaml"))
for config_file in config_files:
try:
config = load_bot_config(config_file)
platform = get_platform_from_config(config)
listener_type = config.get('filters', {}).get('listener_type', 'unknown')
results["valid_configs"].append({
"file": config_file,
"name": config.get("name"),
"platform": platform.value,
"listener": listener_type,
"enabled": config.get("enabled", True)
})
# 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__":
config = load_bot_config("bots/bot-sniper.yaml")
print_config_summary(config)
# 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']}")
+7 -7
View File
@@ -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
-140
View File
@@ -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("<Q", 6966180631402821399)
class BondingCurveState:
"""Represents the state of a pump.fun bonding curve."""
_STRUCT = Struct(
"virtual_token_reserves" / Int64ul,
"virtual_sol_reserves" / Int64ul,
"real_token_reserves" / Int64ul,
"real_sol_reserves" / Int64ul,
"token_total_supply" / Int64ul,
"complete" / Flag,
"creator" / Bytes(32), # Added new creator field - 32 bytes for Pubkey
)
def __init__(self, data: bytes) -> 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
+2 -4
View File
@@ -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
+47 -67
View File
@@ -1,82 +1,62 @@
"""
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
from typing import Final
from solders.pubkey import Pubkey
# Constants
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(
"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."""
PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111")
TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
)
ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
)
RENT: Final[Pubkey] = Pubkey.from_string(
"SysvarRent111111111111111111111111111111111"
)
SOL: Final[Pubkey] = Pubkey.from_string(
"So11111111111111111111111111111111111111112"
)
@dataclass
class PumpAddresses:
"""Pump.fun program addresses."""
PROGRAM: Final[Pubkey] = Pubkey.from_string(
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
)
GLOBAL: Final[Pubkey] = Pubkey.from_string(
"4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"
)
EVENT_AUTHORITY: Final[Pubkey] = Pubkey.from_string(
"Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"
)
FEE: Final[Pubkey] = Pubkey.from_string(
"CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"
)
LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string(
"39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"
)
@staticmethod
def find_global_volume_accumulator() -> Pubkey:
"""
Derive the Program Derived Address (PDA) for the global volume accumulator.
"""System-level Solana addresses shared across all platforms."""
# Reference the module-level constants
SYSTEM_PROGRAM = SYSTEM_PROGRAM
TOKEN_PROGRAM = TOKEN_PROGRAM
ASSOCIATED_TOKEN_PROGRAM = ASSOCIATED_TOKEN_PROGRAM
RENT = RENT
SOL_MINT = SOL_MINT
@classmethod
def get_all_system_addresses(cls) -> dict[str, Pubkey]:
"""Get all system addresses as a dictionary.
Returns:
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,
}
+378
View File
@@ -0,0 +1,378 @@
"""
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 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.
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
+24 -3
View File
@@ -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
-190
View File
@@ -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("<Q", ix_data[:8])[0]
if discriminator != self.CREATE_DISCRIMINATOR:
continue
# Found a create instruction, decode it
create_ix = next(
(
instr
for instr in self._idl["instructions"]
if instr["name"] == "create"
),
None,
)
if not create_ix:
continue
# Get account keys for this instruction
account_keys = [
transaction.message.account_keys[index] for index in ix.accounts
]
# Decode instruction arguments
decoded_args = self._decode_create_instruction(
ix_data, create_ix, account_keys
)
creator = Pubkey.from_string(decoded_args["creator"])
creator_vault = self._find_creator_vault(creator)
return TokenInfo(
name=decoded_args["name"],
symbol=decoded_args["symbol"],
uri=decoded_args["uri"],
mint=Pubkey.from_string(decoded_args["mint"]),
bonding_curve=Pubkey.from_string(decoded_args["bondingCurve"]),
associated_bonding_curve=Pubkey.from_string(
decoded_args["associatedBondingCurve"]
),
user=Pubkey.from_string(decoded_args["user"]),
creator=creator,
creator_vault=creator_vault,
)
except Exception as e:
logger.error(f"Error processing transaction: {e!s}")
return None
def _decode_create_instruction(
self, ix_data: bytes, ix_def: dict[str, Any], accounts: list[Pubkey]
) -> 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("<I", ix_data, offset)[0]
offset += 4
value = ix_data[offset : offset + length].decode("utf-8")
offset += length
elif arg["type"] == "pubkey":
value = base58.b58encode(ix_data[offset : offset + 32]).decode("utf-8")
offset += 32
else:
logger.warning(f"Unsupported type: {arg['type']}")
value = None
args[arg["name"]] = value
args["mint"] = str(accounts[0])
args["bondingCurve"] = str(accounts[2])
args["associatedBondingCurve"] = str(accounts[3])
args["user"] = str(accounts[7])
return args
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
-187
View File
@@ -1,187 +0,0 @@
"""
WebSocket monitoring for pump.fun tokens.
"""
import asyncio
import json
from collections.abc import Awaitable, Callable
import websockets
from solders.pubkey import Pubkey
from monitoring.base_listener import BaseTokenListener
from monitoring.block_event_processor import PumpEventProcessor
from trading.base import TokenInfo
from utils.logger import get_logger
logger = get_logger(__name__)
class BlockListener(BaseTokenListener):
"""WebSocket listener for pump.fun token creation events using blockSubscribe."""
def __init__(self, wss_endpoint: str, pump_program: Pubkey):
"""Initialize token listener.
Args:
wss_endpoint: WebSocket endpoint URL
pump_program: Pump.fun program address
"""
self.wss_endpoint = wss_endpoint
self.pump_program = pump_program
self.event_processor = PumpEventProcessor(pump_program)
self.ping_interval = 20 # seconds
async def listen_for_tokens(
self,
token_callback: Callable[[TokenInfo], Awaitable[None]],
match_string: str | None = None,
creator_address: str | None = None,
) -> None:
"""Listen for new token creations.
Args:
token_callback: Callback function for new tokens
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
while True:
try:
async with websockets.connect(self.wss_endpoint) as websocket:
await self._subscribe_to_program(websocket)
ping_task = asyncio.create_task(self._ping_loop(websocket))
try:
while True:
token_info = await self._wait_for_token_creation(websocket)
if not token_info:
continue
logger.info(
f"New token detected: {token_info.name} ({token_info.symbol})"
)
if match_string and not (
match_string.lower() in token_info.name.lower()
or match_string.lower() in token_info.symbol.lower()
):
logger.info(
f"Token does not match filter '{match_string}'. Skipping..."
)
continue
if (
creator_address
and str(token_info.user) != creator_address
):
logger.info(
f"Token not created by {creator_address}. Skipping..."
)
continue
await token_callback(token_info)
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed. Reconnecting...")
ping_task.cancel()
except Exception as e:
logger.error(f"WebSocket connection error: {e!s}")
logger.info("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
async def _subscribe_to_program(self, websocket) -> None:
"""Subscribe to blocks mentioning the pump.fun program.
Args:
websocket: Active WebSocket connection
"""
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": 1,
"method": "blockSubscribe",
"params": [
{"mentionsAccountOrProgram": str(self.pump_program)},
{
"commitment": "confirmed",
"encoding": "base64", # base64 is faster than other encoding options
"showRewards": False,
"transactionDetails": "full",
"maxSupportedTransactionVersion": 0,
},
],
}
)
await websocket.send(subscription_message)
logger.info(f"Subscribed to blocks mentioning program: {self.pump_program}")
async def _ping_loop(self, websocket) -> None:
"""Keep connection alive with pings.
Args:
websocket: Active WebSocket connection
"""
try:
while True:
await asyncio.sleep(self.ping_interval)
try:
pong_waiter = await websocket.ping()
await asyncio.wait_for(pong_waiter, timeout=10)
except TimeoutError:
logger.warning("Ping timeout - server not responding")
# Force reconnection
await websocket.close()
return
except asyncio.CancelledError:
pass
except Exception as e:
logger.error(f"Ping error: {e!s}")
async def _wait_for_token_creation(self, websocket) -> TokenInfo | None:
"""Wait for token creation event.
Args:
websocket: Active WebSocket connection
Returns:
TokenInfo if a token creation is found, None otherwise
"""
try:
response = await asyncio.wait_for(websocket.recv(), timeout=30)
data = json.loads(response)
if "method" not in data or data["method"] != "blockNotification":
return None
if "params" not in data or "result" not in data["params"]:
return None
block_data = data["params"]["result"]
if "value" not in block_data or "block" not in block_data["value"]:
return None
block = block_data["value"]["block"]
if "transactions" not in block:
return None
for tx in block["transactions"]:
if not isinstance(tx, dict) or "transaction" not in tx:
continue
token_info = self.event_processor.process_transaction(
tx["transaction"][0]
)
if token_info:
return token_info
except TimeoutError:
logger.debug("No data received for 30 seconds")
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed")
raise
except Exception as e:
logger.error(f"Error processing WebSocket message: {e!s}")
return None
-125
View File
@@ -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("<Q", 8576854823835016728)
def __init__(self, pump_program: Pubkey):
"""Initialize event processor.
Args:
pump_program: Pump.fun program address
"""
self.pump_program = pump_program
def process_transaction_data(
self, instruction_data: bytes, accounts: list, keys: list
) -> 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("<I", instruction_data, offset)[0]
offset += 4
# Extract and decode the string
value = instruction_data[offset : offset + length].decode("utf-8")
offset += length
return value
def read_pubkey():
nonlocal offset
value = base58.b58encode(instruction_data[offset : offset + 32]).decode(
"utf-8"
)
offset += 32
return Pubkey.from_string(value)
# Helper to get account key
def get_account_key(index):
if index >= 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
+153
View File
@@ -0,0 +1,153 @@
"""
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,
) -> 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)
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":
# Import the new universal PumpPortal listener
from monitoring.universal_pumpportal_listener import (
UniversalPumpPortalListener,
)
# Validate that requested platforms support PumpPortal
supported_pumpportal_platforms = [Platform.PUMP_FUN, Platform.LETS_BONK]
if platforms:
unsupported = [p for p in platforms if p not in supported_pumpportal_platforms]
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:
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 ["blocks", "geyser", "pumpportal"] # Added pumpportal support
else:
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]
-174
View File
@@ -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("<Q", data[:8])[0]
if discriminator != self.CREATE_DISCRIMINATOR:
logger.info(
f"Skipping non-Create instruction with discriminator: {discriminator}"
)
return None
offset = 8
parsed_data = {}
# Parse fields based on CreateEvent structure
fields = [
("name", "string"),
("symbol", "string"),
("uri", "string"),
("mint", "publicKey"),
("bondingCurve", "publicKey"),
("user", "publicKey"),
("creator", "publicKey"),
]
try:
for field_name, field_type in fields:
if field_type == "string":
length = struct.unpack("<I", data[offset : offset + 4])[0]
offset += 4
value = data[offset : offset + length].decode("utf-8")
offset += length
elif field_type == "publicKey":
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
offset += 32
parsed_data[field_name] = value
return parsed_data
except Exception as e:
logger.error(f"Failed to parse create instruction: {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)],
PumpAddresses.PROGRAM,
)
return derived_address
-167
View File
@@ -1,167 +0,0 @@
"""
WebSocket monitoring for pump.fun tokens using logsSubscribe.
"""
import asyncio
import json
from collections.abc import Awaitable, Callable
import websockets
from solders.pubkey import Pubkey
from monitoring.base_listener import BaseTokenListener
from monitoring.logs_event_processor import LogsEventProcessor
from trading.base import TokenInfo
from utils.logger import get_logger
logger = get_logger(__name__)
class LogsListener(BaseTokenListener):
"""WebSocket listener for pump.fun token creation events using logsSubscribe."""
def __init__(self, wss_endpoint: str, pump_program: Pubkey):
"""Initialize token listener.
Args:
wss_endpoint: WebSocket endpoint URL
pump_program: Pump.fun program address
"""
self.wss_endpoint = wss_endpoint
self.pump_program = pump_program
self.event_processor = LogsEventProcessor(pump_program)
self.ping_interval = 20 # seconds
async def listen_for_tokens(
self,
token_callback: Callable[[TokenInfo], Awaitable[None]],
match_string: str | None = None,
creator_address: str | None = None,
) -> None:
"""Listen for new token creations using logsSubscribe.
Args:
token_callback: Callback function for new tokens
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
while True:
try:
async with websockets.connect(self.wss_endpoint) as websocket:
await self._subscribe_to_logs(websocket)
ping_task = asyncio.create_task(self._ping_loop(websocket))
try:
while True:
token_info = await self._wait_for_token_creation(websocket)
if not token_info:
continue
logger.info(
f"New token detected: {token_info.name} ({token_info.symbol})"
)
if match_string and not (
match_string.lower() in token_info.name.lower()
or match_string.lower() in token_info.symbol.lower()
):
logger.info(
f"Token does not match filter '{match_string}'. Skipping..."
)
continue
if (
creator_address
and str(token_info.user) != creator_address
):
logger.info(
f"Token not created by {creator_address}. Skipping..."
)
continue
await token_callback(token_info)
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed. Reconnecting...")
ping_task.cancel()
except Exception as e:
logger.error(f"WebSocket connection error: {str(e)}")
logger.info("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
async def _subscribe_to_logs(self, websocket) -> None:
"""Subscribe to logs mentioning the pump.fun program.
Args:
websocket: Active WebSocket connection
"""
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": 1,
"method": "logsSubscribe",
"params": [
{"mentions": [str(self.pump_program)]},
{"commitment": "processed"},
],
}
)
await websocket.send(subscription_message)
logger.info(f"Subscribed to logs mentioning program: {self.pump_program}")
# Wait for subscription confirmation
response = await websocket.recv()
response_data = json.loads(response)
if "result" in response_data:
logger.info(f"Subscription confirmed with ID: {response_data['result']}")
else:
logger.warning(f"Unexpected subscription response: {response}")
async def _ping_loop(self, websocket) -> None:
"""Keep connection alive with pings.
Args:
websocket: Active WebSocket connection
"""
try:
while True:
await asyncio.sleep(self.ping_interval)
try:
pong_waiter = await websocket.ping()
await asyncio.wait_for(pong_waiter, timeout=10)
except asyncio.TimeoutError:
logger.warning("Ping timeout - server not responding")
# Force reconnection
await websocket.close()
return
except asyncio.CancelledError:
pass
except Exception as e:
logger.error(f"Ping error: {str(e)}")
async def _wait_for_token_creation(self, websocket) -> TokenInfo | None:
try:
response = await asyncio.wait_for(websocket.recv(), timeout=30)
data = json.loads(response)
if "method" not in data or data["method"] != "logsNotification":
return None
log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", [])
signature = log_data.get("signature", "unknown")
# Use the processor to extract token info
return self.event_processor.process_program_logs(logs, signature)
except asyncio.TimeoutError:
logger.debug("No data received for 30 seconds")
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed")
raise
except Exception as e:
logger.error(f"Error processing WebSocket message: {str(e)}")
return None
+240
View File
@@ -0,0 +1,240 @@
"""
Universal block listener that works with any platform through the interface system.
"""
import asyncio
import json
from collections.abc import Awaitable, Callable
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__)
class UniversalBlockListener(BaseTokenListener):
"""Universal block listener that works with any platform."""
def __init__(
self,
wss_endpoint: str,
platforms: list[Platform] | None = None,
):
"""Initialize universal block listener.
Args:
wss_endpoint: WebSocket endpoint URL
platforms: List of platforms to monitor (if None, monitor all supported platforms)
"""
super().__init__()
self.wss_endpoint = wss_endpoint
self.ping_interval = 20 # seconds
# Import platform factory and get supported platforms
from platforms import platform_factory
if platforms is None:
# Monitor all supported platforms
self.platforms = platform_factory.get_supported_platforms()
else:
self.platforms = platforms
# Get event parsers for all platforms
self.platform_parsers = {}
self.platform_program_ids = []
for platform in self.platforms:
try:
# Create a simple dummy client that doesn't start blockhash updater
from core.client import SolanaClient
# Create a mock client class to avoid network operations
class DummyClient(SolanaClient):
def __init__(self):
# 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
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,
token_callback: Callable[[TokenInfo], Awaitable[None]],
match_string: str | None = None,
creator_address: str | None = None,
) -> None:
"""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_programs(websocket)
ping_task = asyncio.create_task(self._ping_loop(websocket))
try:
while True:
token_info = await self._wait_for_token_creation(websocket)
if not token_info:
continue
logger.info(
f"New token detected: {token_info.name} ({token_info.symbol}) 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()
):
logger.info(
f"Token does not match filter '{match_string}'. Skipping..."
)
continue
if creator_address and str(token_info.user) != creator_address:
logger.info(
f"Token not created by {creator_address}. Skipping..."
)
continue
await token_callback(token_info)
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed. Reconnecting...")
ping_task.cancel()
except Exception:
logger.exception("WebSocket connection error")
logger.info("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
async def _subscribe_to_programs(self, websocket) -> None:
"""Subscribe to blocks mentioning any of the monitored program IDs.
Args:
websocket: Active WebSocket connection
"""
# 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: {program_id}")
async def _ping_loop(self, websocket) -> None:
"""Keep connection alive with pings.
Args:
websocket: Active WebSocket connection
"""
try:
while True:
await asyncio.sleep(self.ping_interval)
try:
pong_waiter = await websocket.ping()
await asyncio.wait_for(pong_waiter, timeout=10)
except TimeoutError:
logger.warning("Ping timeout - server not responding")
# Force reconnection
await websocket.close()
return
except asyncio.CancelledError:
pass
except Exception:
logger.exception("Ping error")
async def _wait_for_token_creation(self, websocket) -> TokenInfo | None:
"""Wait for token creation event 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)
if "method" not in data or data["method"] != "blockNotification":
return None
if "params" not in data or "result" not in data["params"]:
return None
block_data = data["params"]["result"]
if "value" not in block_data or "block" not in block_data["value"]:
return None
block = block_data["value"]["block"]
if "transactions" not in block:
return None
# 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
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")
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed")
raise
except Exception:
logger.exception("Error processing WebSocket message")
return None
@@ -1,42 +1,36 @@
"""
Geyser monitoring for pump.fun tokens.
Universal Geyser listener that works with any platform through the interface system.
"""
import asyncio
from collections.abc import Awaitable, Callable
import grpc
from solders.pubkey import Pubkey
from geyser.generated import geyser_pb2, geyser_pb2_grpc
from 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 platform_factory
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.
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
"""
"""Initialize universal Geyser listener."""
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,11 +38,46 @@ 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)
if platforms is None:
self.platforms = platform_factory.get_supported_platforms()
else:
self.platforms = platforms
# Get event parsers for all platforms
self.platform_parsers = {}
self.platform_program_ids = set()
for platform in self.platforms:
try:
# Create a simple dummy client that doesn't start blockhash updater
from core.client import SolanaClient
# Create a mock client class to avoid network operations
class DummyClient(SolanaClient):
def __init__(self):
# 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())
logger.info(f"Registered platform {platform.value} with program ID {parser.get_program_id()}")
except Exception as e:
logger.warning(f"Could not register platform {platform.value}: {e}")
async def _create_geyser_connection(self):
"""Establish a secure connection to the Geyser endpoint."""
if self.auth_type == "x-token":
auth = grpc.metadata_call_credentials(
lambda _, callback: callback(
@@ -63,15 +92,20 @@ class GeyserListener(BaseTokenListener):
)
creds = grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth)
channel = grpc.aio.secure_channel(self.geyser_endpoint, creds)
return geyser_pb2_grpc.GeyserStub(channel), channel
def _create_subscription_request(self):
"""Create a subscription request for 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
@@ -81,22 +115,19 @@ class GeyserListener(BaseTokenListener):
match_string: str | None = None,
creator_address: str | None = None,
) -> None:
"""Listen for new token creations using Geyser subscription.
"""Listen for new token creations using Geyser subscription."""
if not self.platform_parsers:
logger.error("No platform parsers available. Cannot listen for tokens.")
return
Args:
token_callback: Callback function for new tokens
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
while True:
try:
stub, channel = await self._create_geyser_connection()
request = self._create_subscription_request()
logger.info(f"Connected to Geyser endpoint: {self.geyser_endpoint}")
logger.info(
f"Monitoring for transactions involving program: {self.pump_program}"
)
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 +136,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()
@@ -125,27 +157,23 @@ class GeyserListener(BaseTokenListener):
await token_callback(token_info)
except grpc.aio.AioRpcError as e:
logger.error(f"gRPC error: {e.details()}")
except Exception as e:
if isinstance(e, grpc.aio.AioRpcError):
logger.exception(f"gRPC error: {e.details()}")
else:
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)
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
@@ -155,25 +183,28 @@ class GeyserListener(BaseTokenListener):
if msg is None:
return None
from solders.pubkey import Pubkey
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
except Exception:
logger.exception("Error processing Geyser update")
return None
+212
View File
@@ -0,0 +1,212 @@
"""
Universal logs listener that works with any platform through the interface system.
"""
import asyncio
import json
from collections.abc import Awaitable, Callable
import websockets
from interfaces.core import Platform, TokenInfo
from monitoring.base_listener import BaseTokenListener
from utils.logger import get_logger
logger = get_logger(__name__)
class UniversalLogsListener(BaseTokenListener):
"""Universal logs listener that works with any platform."""
def __init__(
self,
wss_endpoint: str,
platforms: list[Platform] | None = None,
):
"""Initialize universal logs listener.
Args:
wss_endpoint: WebSocket endpoint URL
platforms: List of platforms to monitor (if None, monitor all supported platforms)
"""
super().__init__()
self.wss_endpoint = wss_endpoint
self.ping_interval = 20 # seconds
# Import platform factory and get supported platforms
from platforms import platform_factory
if platforms is None:
# Monitor all supported platforms
self.platforms = platform_factory.get_supported_platforms()
else:
self.platforms = platforms
# Get event parsers for all platforms
self.platform_parsers = {}
self.platform_program_ids = []
for platform in self.platforms:
try:
# Create a simple dummy client that doesn't start blockhash updater
from core.client import SolanaClient
# Create a mock client class to avoid network operations
class DummyClient(SolanaClient):
def __init__(self):
# 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()))
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,
token_callback: Callable[[TokenInfo], Awaitable[None]],
match_string: str | None = None,
creator_address: str | None = None,
) -> None:
"""Listen for new token creations using logsSubscribe.
Args:
token_callback: Callback function for new tokens
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
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_logs(websocket)
ping_task = asyncio.create_task(self._ping_loop(websocket))
try:
while True:
token_info = await self._wait_for_token_creation(websocket)
if not token_info:
continue
logger.info(
f"New token detected: {token_info.name} ({token_info.symbol}) 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()
):
logger.info(
f"Token does not match filter '{match_string}'. Skipping..."
)
continue
if creator_address and str(token_info.user) != creator_address:
logger.info(
f"Token not created by {creator_address}. Skipping..."
)
continue
await token_callback(token_info)
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed. Reconnecting...")
ping_task.cancel()
except Exception:
logger.exception("WebSocket connection error")
logger.info("Reconnecting in 5 seconds...")
await asyncio.sleep(5)
async def _subscribe_to_logs(self, websocket) -> None:
"""Subscribe to logs mentioning any of the monitored program IDs.
Args:
websocket: Active WebSocket connection
"""
# Subscribe to logs for all monitored platforms
for i, program_id in enumerate(self.platform_program_ids):
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": i + 1,
"method": "logsSubscribe",
"params": [
{"mentions": [program_id]},
{"commitment": "processed"},
],
}
)
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}")
async def _ping_loop(self, websocket) -> None:
"""Keep connection alive with pings."""
try:
while True:
await asyncio.sleep(self.ping_interval)
try:
pong_waiter = await websocket.ping()
await asyncio.wait_for(pong_waiter, timeout=10)
except TimeoutError:
logger.warning("Ping timeout - server not responding")
await websocket.close()
return
except asyncio.CancelledError:
pass
except Exception:
logger.exception("Ping error")
async def _wait_for_token_creation(self, websocket) -> TokenInfo | None:
"""Wait for token creation events from any platform."""
try:
response = await asyncio.wait_for(websocket.recv(), timeout=30)
data = json.loads(response)
if "method" not in data or data["method"] != "logsNotification":
return None
log_data = data["params"]["result"]["value"]
logs = log_data.get("logs", [])
signature = log_data.get("signature", "unknown")
# 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
return None
except TimeoutError:
logger.debug("No data received for 30 seconds")
except websockets.exceptions.ConnectionClosed:
logger.warning("WebSocket connection closed")
raise
except Exception:
logger.exception("Error processing WebSocket message")
return None
@@ -1,5 +1,5 @@
"""
PumpPortal monitoring for pump.fun tokens.
Universal PumpPortal listener that works with multiple platforms.
"""
import asyncio
@@ -7,34 +7,58 @@ 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.pumpportal_event_processor import PumpPortalEventProcessor
from trading.base import TokenInfo
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,
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
pumpportal_url: PumpPortal WebSocket URL
platforms: List of platforms to monitor (if None, monitor all supported platforms)
"""
self.pump_program = pump_program
super().__init__()
self.pumpportal_url = pumpportal_url
self.event_processor = PumpPortalEventProcessor(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,
@@ -62,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()
@@ -74,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)
@@ -131,8 +156,8 @@ class PumpPortalListener(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.
@@ -148,27 +173,44 @@ 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")
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
return None
+360
View File
@@ -0,0 +1,360 @@
"""
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 with IDL support.
"""
from dataclasses import dataclass
from typing import Any
from core.client import SolanaClient
from interfaces.core import (
AddressProvider,
CurveManager,
EventParser,
InstructionBuilder,
Platform,
)
from utils.idl_manager import get_idl_manager, has_idl_support
from utils.logger import get_logger
logger = get_logger(__name__)
@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[tuple[Platform, str], PlatformImplementations] = {}
def register_platform(
self,
platform: Platform,
address_provider_class: type[AddressProvider],
instruction_builder_class: type[InstructionBuilder],
curve_manager_class: type[CurveManager],
event_parser_class: type[EventParser]
) -> 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 with IDL support.
Args:
platform: Platform to create implementations for
client: Solana RPC client
**kwargs: Additional arguments for implementation constructors
Returns:
PlatformImplementations containing all interface implementations
Raises:
ValueError: If platform is not registered
"""
if platform not in self._implementations:
raise ValueError(f"Platform {platform} is not registered")
# Use client address as cache key to allow multiple clients
cache_key = (platform, str(client.rpc_endpoint))
# Check if we already have instances for this platform + client combo
if cache_key in self._instances:
return self._instances[cache_key]
impl_classes = self._implementations[platform]
# Check if platform has IDL support and prepare IDL parser
idl_parser = None
if has_idl_support(platform):
try:
idl_manager = get_idl_manager()
idl_parser = idl_manager.get_parser(platform, verbose=kwargs.get('verbose_idl', False))
logger.info(f"IDL parser loaded for {platform.value} platform implementations")
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']()
# For platforms with IDL support, pass the parser to relevant classes
if idl_parser and platform in [Platform.LETS_BONK, Platform.PUMP_FUN]:
instruction_builder = impl_classes['instruction_builder'](idl_parser=idl_parser)
curve_manager = impl_classes['curve_manager'](client, idl_parser=idl_parser)
event_parser = impl_classes['event_parser'](idl_parser=idl_parser)
else:
# Fallback for platforms without IDL support
instruction_builder = impl_classes['instruction_builder']()
curve_manager = impl_classes['curve_manager'](client)
event_parser = impl_classes['event_parser']()
implementations = PlatformImplementations(
address_provider=address_provider,
instruction_builder=instruction_builder,
curve_manager=curve_manager,
event_parser=event_parser
)
# Cache the instances
self._instances[cache_key] = implementations
return implementations
def get_platform_implementations(self, platform: Platform, client_endpoint: str) -> PlatformImplementations | None:
"""Get cached platform implementations.
Args:
platform: Platform to get implementations for
client_endpoint: Client endpoint for cache lookup
Returns:
PlatformImplementations if available, None otherwise
"""
cache_key = (platform, client_endpoint)
return self._instances.get(cache_key)
def get_supported_platforms(self) -> list[Platform]:
"""Get list of supported platforms.
Returns:
List of registered platforms
"""
return list(self._implementations.keys())
def is_platform_supported(self, platform: Platform) -> bool:
"""Check if a platform is supported.
Args:
platform: Platform to check
Returns:
True if platform is registered, False otherwise
"""
return platform in self._implementations
def clear_implementation_cache(self, platform: Platform | None = None) -> None:
"""Clear cached platform implementations.
Args:
platform: Specific platform to clear, or None to clear all
"""
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 with IDL support."""
def __init__(self):
self.registry = PlatformRegistry()
self._setup_default_platforms()
def _setup_default_platforms(self) -> None:
"""Setup default platform registrations."""
# Import and register pump.fun platform
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}")
# Import and register LetsBonk platform
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,
platform: Platform,
client: SolanaClient,
**config: Any
) -> PlatformImplementations:
"""Create all implementations for a specific platform.
Args:
platform: Platform to create implementations for
client: Solana RPC client
**config: Platform-specific configuration (including verbose_idl)
Returns:
PlatformImplementations containing all interface implementations
"""
return self.registry.create_platform_implementations(platform, client, **config)
def get_address_provider(self, platform: Platform, client: SolanaClient) -> AddressProvider:
"""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()
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
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
)
+21
View File
@@ -0,0 +1,21 @@
"""
LetsBonk platform exports.
This module provides convenient imports for the LetsBonk platform implementations.
Platform registration is now handled by the main platform factory.
"""
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',
'LetsBonkPumpPortalProcessor'
]
+294
View File
@@ -0,0 +1,294 @@
"""
LetsBonk implementation of AddressProvider interface.
This module provides all LetsBonk (Raydium LaunchLab) 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 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."""
@property
def platform(self) -> Platform:
"""Get the platform this provider serves."""
return Platform.LETS_BONK
@property
def program_id(self) -> Pubkey:
"""Get the main program ID for this platform."""
return LetsBonkAddresses.PROGRAM
def get_system_addresses(self) -> dict[str, Pubkey]:
"""Get all system addresses required for LetsBonk.
Returns:
Dictionary mapping address names to Pubkey objects
"""
# Get system addresses from the single source of truth
system_addresses = SystemAddresses.get_all_system_addresses()
# Add LetsBonk specific addresses
letsbonk_addresses = {
# Raydium LaunchLab specific addresses
"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.
For LetsBonk, this derives the pool state PDA using base_mint and WSOL.
Args:
base_mint: Base token mint address
quote_mint: Quote token mint (defaults to WSOL)
Returns:
Pool state address
"""
if quote_mint is None:
quote_mint = SystemAddresses.SOL_MINT
pool_state, _ = Pubkey.find_program_address(
[b"pool", bytes(base_mint), bytes(quote_mint)],
LetsBonkAddresses.PROGRAM
)
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.
Args:
user: User's wallet address
mint: Token mint address
Returns:
User's associated token account address
"""
return get_associated_token_address(user, mint)
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
"""Get LetsBonk-specific additional accounts needed for trading.
Args:
token_info: Token information
Returns:
Dictionary of additional account addresses
"""
accounts = {}
# Add pool state - derive if not present or use existing
if token_info.pool_state:
accounts["pool_state"] = token_info.pool_state
else:
accounts["pool_state"] = self.derive_pool_address(token_info.mint)
# Add vault addresses - derive if not present or use existing
if token_info.base_vault:
accounts["base_vault"] = token_info.base_vault
else:
accounts["base_vault"] = self.derive_base_vault(token_info.mint)
if token_info.quote_vault:
accounts["quote_vault"] = token_info.quote_vault
else:
accounts["quote_vault"] = self.derive_quote_vault(token_info.mint)
# Derive authority PDA
accounts["authority"] = self.derive_authority_pda()
# Derive event authority PDA
accounts["event_authority"] = self.derive_event_authority_pda()
return accounts
def derive_authority_pda(self) -> Pubkey:
"""Derive the authority PDA for Raydium LaunchLab.
This PDA acts as the authority for pool vault operations.
Returns:
Authority PDA address
"""
AUTH_SEED = b"vault_auth_seed"
authority_pda, _ = Pubkey.find_program_address(
[AUTH_SEED],
LetsBonkAddresses.PROGRAM
)
return authority_pda
def derive_event_authority_pda(self) -> Pubkey:
"""Derive the event authority PDA for Raydium LaunchLab.
This PDA is used for emitting program events during swaps.
Returns:
Event authority PDA address
"""
EVENT_AUTHORITY_SEED = b"__event_authority"
event_authority_pda, _ = Pubkey.find_program_address(
[EVENT_AUTHORITY_SEED],
LetsBonkAddresses.PROGRAM
)
return event_authority_pda
def create_wsol_account_with_seed(self, payer: Pubkey, seed: str) -> Pubkey:
"""Create a WSOL account address using createAccountWithSeed pattern.
Args:
payer: The account that will pay for and own the new account
seed: String seed for deterministic account generation
Returns:
New WSOL account address
"""
return Pubkey.create_with_seed(payer, seed, SystemAddresses.TOKEN_PROGRAM)
def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
"""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": 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["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,
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
"event_authority": additional_accounts["event_authority"],
"program": LetsBonkAddresses.PROGRAM,
}
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": 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["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,
"quote_token_program": SystemAddresses.TOKEN_PROGRAM,
"event_authority": additional_accounts["event_authority"],
"program": LetsBonkAddresses.PROGRAM,
}
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": SystemAddresses.SOL_MINT,
"owner": user,
"system_program": SystemAddresses.SYSTEM_PROGRAM,
"token_program": SystemAddresses.TOKEN_PROGRAM,
"rent": SystemAddresses.RENT,
}
+244
View File
@@ -0,0 +1,244 @@
"""
LetsBonk implementation of CurveManager interface.
This module handles LetsBonk (Raydium LaunchLab) specific pool operations
by implementing the CurveManager interface using IDL-based decoding.
"""
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.idl_parser import IDLParser
from utils.logger import get_logger
logger = get_logger(__name__)
class LetsBonkCurveManager(CurveManager):
"""LetsBonk (Raydium LaunchLab) implementation of CurveManager interface."""
def __init__(self, client: SolanaClient, idl_parser: IDLParser):
"""Initialize LetsBonk curve manager with injected IDL parser.
Args:
client: Solana RPC client
idl_parser: Pre-loaded IDL parser for LetsBonk platform
"""
self.client = client
self.address_provider = LetsBonkAddressProvider()
self._idl_parser = idl_parser
logger.info("LetsBonk curve manager initialized with injected IDL parser")
@property
def platform(self) -> Platform:
"""Get the platform this manager serves."""
return Platform.LETS_BONK
async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]:
"""Get the current state of a LetsBonk pool.
Args:
pool_address: Address of the pool state account
Returns:
Dictionary containing pool state data
"""
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 using injected IDL parser
pool_state_data = self._decode_pool_state_with_idl(account.data)
return pool_state_data
except Exception as e:
logger.exception("Failed to get pool state")
raise ValueError(f"Invalid pool state: {e!s}")
async def calculate_price(self, pool_address: Pubkey) -> float:
"""Calculate current token price from pool state.
Args:
pool_address: Address of the pool state
Returns:
Current token price in SOL
"""
pool_state = await self.get_pool_state(pool_address)
# Use virtual reserves for price calculation
virtual_base = pool_state["virtual_base"]
virtual_quote = pool_state["virtual_quote"]
if virtual_base <= 0 or virtual_quote <= 0:
raise ValueError("Invalid reserve state")
# Price = quote_reserves / base_reserves (how much SOL per token)
price_lamports = virtual_quote / virtual_base
price_sol = price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
return price_sol
async def calculate_buy_amount_out(
self,
pool_address: Pubkey,
amount_in: int
) -> 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_with_idl(self, data: bytes) -> dict[str, Any]:
"""Decode pool state data using injected IDL parser.
Args:
data: Raw account data
Returns:
Dictionary with decoded pool state
Raises:
ValueError: If IDL parsing fails
"""
# Use injected IDL parser to decode PoolState account data
decoded_pool_state = self._idl_parser.decode_account_data(
data,
"PoolState",
skip_discriminator=True
)
if not decoded_pool_state:
raise ValueError("Failed to decode pool state with IDL parser")
# Extract the fields we need for trading calculations
# Based on the PoolState structure from the IDL
pool_data = {
"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),
}
# 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 validate_pool_state_structure(self, pool_address: Pubkey) -> bool:
"""Validate that the pool state structure matches IDL expectations.
Args:
pool_address: Address of the pool state
Returns:
True if structure is valid, False otherwise
"""
try:
# This would be used during development/testing to ensure
# the IDL parsing is working correctly
pool_state = await self.get_pool_state(pool_address)
required_fields = [
"virtual_base", "virtual_quote",
"real_base", "real_quote"
]
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:
logger.exception("Pool state validation failed")
return False
+330
View File
@@ -0,0 +1,330 @@
"""
LetsBonk implementation of EventParser interface.
This module parses LetsBonk-specific token creation events from various sources
by implementing the EventParser interface with IDL-based parsing.
"""
import base64
import struct
from time import monotonic
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 with IDL-based parsing."""
def __init__(self, idl_parser: IDLParser):
"""Initialize LetsBonk event parser with injected IDL parser.
Args:
idl_parser: Pre-loaded IDL parser for LetsBonk platform
"""
self.address_provider = LetsBonkAddressProvider()
self._idl_parser = idl_parser
# Get discriminators from injected IDL parser
discriminators = self._idl_parser.get_instruction_discriminators()
self._initialize_discriminator_bytes = discriminators["initialize"]
self._initialize_discriminator = struct.unpack("<Q", self._initialize_discriminator_bytes)[0]
logger.info("LetsBonk event parser initialized with injected IDL parser")
@property
def platform(self) -> 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 using injected IDL parser.
Args:
instruction_data: Raw instruction data
accounts: List of account indices
account_keys: List of account public keys
Returns:
TokenInfo if token creation found, None otherwise
"""
if not instruction_data.startswith(self._initialize_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 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
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 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(
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,
base_vault=base_vault,
quote_vault=quote_vault,
user=creator,
creator=creator,
creation_timestamp=monotonic(),
)
except Exception as e:
logger.debug(f"Failed to parse initialize instruction: {e}")
return None
def parse_token_creation_from_geyser(
self,
transaction_info: Any
) -> 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.get_system_addresses()["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 as e:
logger.debug(f"Failed to parse geyser transaction: {e}")
return None
def get_program_id(self) -> Pubkey:
"""Get the Raydium LaunchLab program ID this parser monitors.
Returns:
Raydium LaunchLab program ID
"""
return self.address_provider.program_id
def get_instruction_discriminators(self) -> list[bytes]:
"""Get instruction discriminators for token creation.
Returns:
List of discriminator bytes to match
"""
return [self._initialize_discriminator_bytes]
def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
"""Parse token creation from block data (for block listener).
Args:
block_data: Block data from WebSocket
Returns:
TokenInfo if token creation found, None otherwise
"""
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("<Q", ix_data[:8])[0]
if discriminator == self._initialize_discriminator:
# 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, account_keys_bytes
)
if token_info:
return token_info
except Exception as e:
logger.debug(f"Failed to parse block transaction: {e}")
continue
# Handle already decoded transaction data
elif isinstance(tx_data, dict) and "message" in tx_data:
try:
message = tx_data["message"]
if "instructions" not in message or "accountKeys" not in message:
continue
for ix in message["instructions"]:
if "programIdIndex" not in ix or "accounts" not in ix or "data" not in ix:
continue
program_idx = ix["programIdIndex"]
if program_idx >= 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("<Q", ix_data[:8])[0]
if discriminator == self._initialize_discriminator:
if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
continue
# Convert account keys to bytes for parsing
account_keys_bytes = [
Pubkey.from_string(key).to_bytes()
for key in message["accountKeys"]
]
token_info = self.parse_token_creation_from_instruction(
ix_data, ix["accounts"], account_keys_bytes
)
if token_info:
return token_info
except Exception as e:
logger.debug(f"Failed to parse decoded block transaction: {e}")
continue
return None
except Exception as e:
logger.debug(f"Failed to parse block data: {e}")
return None
@@ -0,0 +1,412 @@
"""
LetsBonk implementation of InstructionBuilder interface.
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_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
logger = get_logger(__name__)
class LetsBonkInstructionBuilder(InstructionBuilder):
"""LetsBonk (Raydium LaunchLab) implementation of InstructionBuilder interface with IDL-based discriminators."""
def __init__(self, idl_parser: IDLParser):
"""Initialize LetsBonk instruction builder with injected IDL parser.
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 injected IDL parser")
@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
SystemAddresses.TOKEN_PROGRAM, # token program
)
instructions.append(ata_instruction)
# 2. Create WSOL account with seed (temporary account for the transaction)
wsol_seed = self._generate_wsol_seed(user)
wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed)
# Account creation cost + amount to spend
account_creation_lamports = TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE
total_lamports = amount_in + account_creation_lamports
create_wsol_ix = create_account_with_seed(
CreateAccountWithSeedParams(
from_pubkey=user,
to_pubkey=wsol_account,
base=user,
seed=wsol_seed,
lamports=total_lamports,
space=TOKEN_ACCOUNT_SIZE,
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=user, is_signer=True, is_writable=True), # payer
AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), # authority
AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), # global_config
AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), # platform_config
AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), # pool_state
AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), # user_base_token
AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token (WSOL account)
AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), # base_vault
AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), # quote_vault
AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), # base_token_mint
AccountMeta(pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False), # quote_token_mint
AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # base_token_program
AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # quote_token_program
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), # event_authority
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), # program
]
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
SHARE_FEE_RATE = 0 # No sharing fee
instruction_data = (
self._buy_exact_in_discriminator +
struct.pack("<Q", amount_in) + # amount_in (u64) - SOL to spend
struct.pack("<Q", minimum_amount_out) + # minimum_amount_out (u64) - min tokens
struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
)
buy_instruction = Instruction(
program_id=accounts_info["program"],
data=instruction_data,
accounts=buy_accounts
)
instructions.append(buy_instruction)
# 5. Close WSOL account to reclaim SOL
close_wsol_ix = self._create_close_account_instruction(
wsol_account,
user,
user
)
instructions.append(close_wsol_ix)
return instructions
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 LetsBonk using sell_exact_in.
Args:
token_info: Token information
user: User's wallet address
amount_in: Amount of tokens to sell (raw token units)
minimum_amount_out: Minimum SOL expected (in lamports)
address_provider: Platform address provider
Returns:
List of instructions needed for the sell operation
"""
instructions = []
# Get all required accounts
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
# 1. Create WSOL account with seed (to receive SOL)
wsol_seed = self._generate_wsol_seed(user)
wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed)
# Minimal account creation cost
account_creation_lamports = TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE
create_wsol_ix = create_account_with_seed(
CreateAccountWithSeedParams(
from_pubkey=user,
to_pubkey=wsol_account,
base=user,
seed=wsol_seed,
lamports=account_creation_lamports,
space=TOKEN_ACCOUNT_SIZE,
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=user, is_signer=True, is_writable=True), # payer
AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), # authority
AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), # global_config
AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), # platform_config
AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), # pool_state
AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), # user_base_token (tokens being sold)
AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token (WSOL received)
AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), # base_vault (receives tokens)
AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), # quote_vault (sends WSOL)
AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), # base_token_mint
AccountMeta(pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False), # quote_token_mint
AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # base_token_program
AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # quote_token_program
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), # event_authority
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), # program
]
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
SHARE_FEE_RATE = 0 # No sharing fee
instruction_data = (
self._sell_exact_in_discriminator +
struct.pack("<Q", amount_in) + # amount_in (u64) - tokens to sell
struct.pack("<Q", minimum_amount_out) + # minimum_amount_out (u64) - min SOL
struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
)
sell_instruction = Instruction(
program_id=accounts_info["program"],
data=instruction_data,
accounts=sell_accounts
)
instructions.append(sell_instruction)
# 4. Close WSOL account to reclaim SOL
close_wsol_ix = self._create_close_account_instruction(
wsol_account,
user,
user
)
instructions.append(close_wsol_ix)
return instructions
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
"""
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"],
token_info.mint,
SystemAddresses.SOL_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"],
token_info.mint,
SystemAddresses.SOL_MINT,
accounts_info["program"],
]
def _generate_wsol_seed(self, user: Pubkey) -> str:
"""Generate a unique seed for WSOL account creation.
Args:
user: User's wallet address
Returns:
Unique seed string for WSOL account
"""
# Generate a unique seed based on timestamp and user pubkey
seed_data = f"{int(time.time())}{user!s}"
return hashlib.sha256(seed_data.encode()).hexdigest()[:32]
def _create_initialize_account_instruction(
self,
account: Pubkey,
mint: Pubkey,
owner: Pubkey
) -> 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.
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
@@ -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:
logger.exception("Failed to process PumpPortal LetsBonk token data")
return None
+21
View File
@@ -0,0 +1,21 @@
"""
Pump.Fun platform exports.
This module provides convenient imports for the pump.fun platform implementations.
Platform registration is now handled by the main platform factory.
"""
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',
'PumpFunEventParser',
'PumpFunInstructionBuilder',
'PumpFunPumpPortalProcessor'
]
+278
View File
@@ -0,0 +1,278 @@
"""
Pump.Fun implementation of AddressProvider interface.
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 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."""
@property
def platform(self) -> Platform:
"""Get the platform this provider serves."""
return Platform.PUMP_FUN
@property
def program_id(self) -> Pubkey:
"""Get the main program ID for this platform."""
return PumpFunAddresses.PROGRAM
def get_system_addresses(self) -> dict[str, Pubkey]:
"""Get all system addresses required for pump.fun.
Returns:
Dictionary mapping address names to Pubkey objects
"""
# Get system addresses from the single source of truth
system_addresses = SystemAddresses.get_all_system_addresses()
# Add pump.fun specific addresses
pumpfun_addresses = {
# Pump.fun specific addresses
"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.
For pump.fun, this is the bonding curve PDA derived from the mint.
Args:
base_mint: Token mint address
quote_mint: Not used for pump.fun (SOL is always the quote)
Returns:
Bonding curve address
"""
bonding_curve, _ = Pubkey.find_program_address(
[b"bonding-curve", bytes(base_mint)],
PumpFunAddresses.PROGRAM
)
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
"""
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.
Args:
creator: Creator address
Returns:
Creator vault address
"""
creator_vault, _ = Pubkey.find_program_address(
[b"creator-vault", bytes(creator)],
PumpFunAddresses.PROGRAM
)
return creator_vault
def derive_global_volume_accumulator(self) -> Pubkey:
"""Derive the global volume accumulator PDA.
Returns:
Global volume accumulator address
"""
return PumpFunAddresses.find_global_volume_accumulator()
def derive_user_volume_accumulator(self, user: Pubkey) -> Pubkey:
"""Derive the user volume accumulator PDA.
Args:
user: User address
Returns:
User volume accumulator address
"""
return PumpFunAddresses.find_user_volume_accumulator(user)
def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
"""Get all accounts needed for a buy instruction.
Args:
token_info: Token information
user: User's wallet address
Returns:
Dictionary of account addresses for buy instruction
"""
additional_accounts = self.get_additional_accounts(token_info)
return {
"global": PumpFunAddresses.GLOBAL,
"fee": PumpFunAddresses.FEE,
"mint": token_info.mint,
"bonding_curve": additional_accounts.get("bonding_curve", token_info.bonding_curve),
"associated_bonding_curve": additional_accounts.get("associated_bonding_curve", token_info.associated_bonding_curve),
"user_token_account": self.derive_user_token_account(user, token_info.mint),
"user": user,
"system_program": SystemAddresses.SYSTEM_PROGRAM,
"token_program": SystemAddresses.TOKEN_PROGRAM,
"creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault),
"event_authority": PumpFunAddresses.EVENT_AUTHORITY,
"program": PumpFunAddresses.PROGRAM,
"global_volume_accumulator": self.derive_global_volume_accumulator(),
"user_volume_accumulator": self.derive_user_volume_accumulator(user),
}
def get_sell_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
"""Get all accounts needed for a sell instruction.
Args:
token_info: Token information
user: User's wallet address
Returns:
Dictionary of account addresses for sell instruction
"""
additional_accounts = self.get_additional_accounts(token_info)
return {
"global": PumpFunAddresses.GLOBAL,
"fee": PumpFunAddresses.FEE,
"mint": token_info.mint,
"bonding_curve": additional_accounts.get("bonding_curve", token_info.bonding_curve),
"associated_bonding_curve": additional_accounts.get("associated_bonding_curve", token_info.associated_bonding_curve),
"user_token_account": self.derive_user_token_account(user, token_info.mint),
"user": user,
"system_program": SystemAddresses.SYSTEM_PROGRAM,
"creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault),
"token_program": SystemAddresses.TOKEN_PROGRAM,
"event_authority": PumpFunAddresses.EVENT_AUTHORITY,
"program": PumpFunAddresses.PROGRAM,
}
+322
View File
@@ -0,0 +1,322 @@
"""
Pump.Fun implementation of CurveManager interface.
This module handles pump.fun-specific bonding curve operations
by implementing the CurveManager interface using IDL-based decoding.
"""
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 utils.idl_parser import IDLParser
from utils.logger import get_logger
logger = get_logger(__name__)
class PumpFunCurveManager(CurveManager):
"""Pump.Fun implementation of CurveManager interface using IDL-based decoding."""
def __init__(self, client: SolanaClient, idl_parser: IDLParser):
"""Initialize pump.fun curve manager with injected IDL parser.
Args:
client: Solana RPC client
idl_parser: Pre-loaded IDL parser for pump.fun platform
"""
self.client = client
self._idl_parser = idl_parser
logger.info("Pump.Fun curve manager initialized with injected IDL parser")
@property
def platform(self) -> Platform:
"""Get the platform this manager serves."""
return Platform.PUMP_FUN
async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]:
"""Get the current state of a pump.fun bonding curve.
Args:
pool_address: Address of the bonding curve
Returns:
Dictionary containing bonding curve state data
"""
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}")
# Decode bonding curve state using injected IDL parser
curve_state_data = self._decode_curve_state_with_idl(account.data)
return curve_state_data
except Exception as e:
logger.exception("Failed to get curve state")
raise ValueError(f"Invalid bonding curve state: {e!s}")
async def calculate_price(self, pool_address: Pubkey) -> float:
"""Calculate current token price from bonding curve state.
Args:
pool_address: Address of the bonding curve
Returns:
Current token price in SOL
"""
pool_state = await self.get_pool_state(pool_address)
# Use virtual reserves for price calculation
virtual_token_reserves = pool_state["virtual_token_reserves"]
virtual_sol_reserves = pool_state["virtual_sol_reserves"]
if virtual_token_reserves <= 0:
return 0.0
# Price = sol_reserves / token_reserves
price_lamports = virtual_sol_reserves / virtual_token_reserves
return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
async def calculate_buy_amount_out(
self,
pool_address: Pubkey,
amount_in: int
) -> int:
"""Calculate expected tokens received for a buy operation.
Uses the pump.fun bonding curve formula to calculate token output.
Args:
pool_address: Address of the bonding curve
amount_in: Amount of SOL to spend (in lamports)
Returns:
Expected amount of tokens to receive (in raw token units)
"""
pool_state = await self.get_pool_state(pool_address)
virtual_token_reserves = pool_state["virtual_token_reserves"]
virtual_sol_reserves = pool_state["virtual_sol_reserves"]
# Use virtual reserves for bonding curve calculation
# Formula: tokens_out = (amount_in * virtual_token_reserves) / (virtual_sol_reserves + amount_in)
numerator = amount_in * virtual_token_reserves
denominator = virtual_sol_reserves + amount_in
if denominator == 0:
return 0
tokens_out = numerator // denominator
return tokens_out
async def calculate_sell_amount_out(
self,
pool_address: Pubkey,
amount_in: int
) -> int:
"""Calculate expected SOL received for a sell operation.
Uses the pump.fun bonding curve formula to calculate SOL output.
Args:
pool_address: Address of the bonding curve
amount_in: Amount of tokens to sell (in raw token units)
Returns:
Expected amount of SOL to receive (in lamports)
"""
pool_state = await self.get_pool_state(pool_address)
virtual_token_reserves = pool_state["virtual_token_reserves"]
virtual_sol_reserves = pool_state["virtual_sol_reserves"]
# Use virtual reserves for bonding curve calculation
# Formula: sol_out = (amount_in * virtual_sol_reserves) / (virtual_token_reserves + amount_in)
numerator = amount_in * virtual_sol_reserves
denominator = virtual_token_reserves + amount_in
if denominator == 0:
return 0
sol_out = numerator // denominator
return sol_out
async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]:
"""Get current bonding curve reserves.
Args:
pool_address: Address of the bonding curve
Returns:
Tuple of (token_reserves, sol_reserves) in raw units
"""
pool_state = await self.get_pool_state(pool_address)
return (pool_state["virtual_token_reserves"], pool_state["virtual_sol_reserves"])
def _decode_curve_state_with_idl(self, data: bytes) -> dict[str, Any]:
"""Decode bonding curve state data using injected IDL parser.
Args:
data: Raw account data
Returns:
Dictionary with decoded bonding curve state
Raises:
ValueError: If IDL parsing fails
"""
# Use injected IDL parser to decode BondingCurve account data
decoded_curve_state = self._idl_parser.decode_account_data(
data,
"BondingCurve",
skip_discriminator=True
)
if not decoded_curve_state:
raise ValueError("Failed to decode bonding curve state with IDL parser")
# Extract the fields we need for trading calculations
# Based on the BondingCurve structure from the IDL
curve_data = {
"virtual_token_reserves": decoded_curve_state.get("virtual_token_reserves", 0),
"virtual_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", ""),
}
# Calculate additional metrics
if curve_data["virtual_token_reserves"] > 0:
curve_data["price_per_token"] = (
(curve_data["virtual_sol_reserves"] / curve_data["virtual_token_reserves"])
* (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
)
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:
"""Calculate the expected token amount for a given SOL input.
This is a convenience method that converts between decimal and raw units.
Args:
pool_address: Address of the bonding curve
sol_amount: Amount of SOL to spend (in decimal SOL)
Returns:
Expected token amount (in decimal tokens)
"""
sol_lamports = int(sol_amount * LAMPORTS_PER_SOL)
tokens_raw = await self.calculate_buy_amount_out(pool_address, sol_lamports)
return tokens_raw / 10**TOKEN_DECIMALS
async def calculate_expected_sol(self, pool_address: Pubkey, token_amount: float) -> float:
"""Calculate the expected SOL amount for a given token input.
This is a convenience method that converts between decimal and raw units.
Args:
pool_address: Address of the bonding curve
token_amount: Amount of tokens to sell (in decimal tokens)
Returns:
Expected SOL amount (in decimal SOL)
"""
tokens_raw = int(token_amount * 10**TOKEN_DECIMALS)
sol_lamports = await self.calculate_sell_amount_out(pool_address, tokens_raw)
return sol_lamports / LAMPORTS_PER_SOL
async def is_curve_complete(self, pool_address: Pubkey) -> bool:
"""Check if the bonding curve is complete (migrated to Raydium).
Args:
pool_address: Address of the bonding curve
Returns:
True if curve is complete, False otherwise
"""
pool_state = await self.get_pool_state(pool_address)
return pool_state.get("complete", False)
async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]:
"""Get bonding curve completion progress information.
Args:
pool_address: Address of the bonding curve
Returns:
Dictionary with progress information
"""
pool_state = await self.get_pool_state(pool_address)
# Calculate progress based on SOL raised vs target
# This is approximate since the exact target isn't stored in the curve state
sol_raised = pool_state["real_sol_reserves"] / LAMPORTS_PER_SOL
# Estimate progress based on typical pump.fun graduation requirements
# (This could be made more accurate with additional on-chain data)
estimated_target_sol = 85.0 # Typical pump.fun graduation target
progress_percentage = min((sol_raised / estimated_target_sol) * 100, 100.0)
return {
"complete": pool_state.get("complete", False),
"sol_raised": sol_raised,
"estimated_target_sol": estimated_target_sol,
"progress_percentage": progress_percentage,
"tokens_available": pool_state["virtual_token_reserves"] / 10**TOKEN_DECIMALS,
"market_cap_sol": sol_raised, # Approximate market cap
}
def validate_curve_state_structure(self, pool_address: Pubkey) -> bool:
"""Validate that the curve state structure matches IDL expectations.
Args:
pool_address: Address of the bonding curve
Returns:
True if structure is valid, False otherwise
"""
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:
logger.exception("Curve state validation failed")
return False
+485
View File
@@ -0,0 +1,485 @@
"""
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 event parsing.
"""
import base64
import struct
from time import monotonic
from typing import Any
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 with IDL-based event parsing."""
def __init__(self, idl_parser: IDLParser):
"""Initialize pump.fun event parser with injected IDL parser.
Args:
idl_parser: Pre-loaded IDL parser for pump.fun platform
"""
self._idl_parser = idl_parser
event_discriminators = self._idl_parser.get_event_discriminators()
self._create_event_discriminator_bytes = event_discriminators["CreateEvent"]
self._create_event_discriminator = struct.unpack("<Q", self._create_event_discriminator_bytes)[0]
instruction_discriminators = self._idl_parser.get_instruction_discriminators()
self._create_instruction_discriminator_bytes = instruction_discriminators["create"]
self._create_instruction_discriminator = struct.unpack("<Q", self._create_instruction_discriminator_bytes)[0]
logger.info("Pump.Fun event parser initialized with IDL-based event and instruction parsing")
logger.info(f"CreateEvent discriminator: {self._create_event_discriminator_bytes.hex()}")
logger.info(f"create instruction discriminator: {self._create_instruction_discriminator_bytes.hex()}")
@property
def platform(self) -> 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 using IDL event parsing.
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 transaction
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
logger.info(f"🔍 Parsing token creation from logs for signature: {signature}")
# Look for event data in the logs (CreateEvent data!)
# We need to find the Program data that comes after "Instruction: Create"
try:
create_instruction_found = False
program_data_entries = []
# First, collect all Program data entries and note when Create instruction happens
for i, log in enumerate(logs):
if "Program log: Instruction: Create" in log:
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("<Q", discriminator)[0]
logger.info(f"🔍 Program data discriminator: {discriminator.hex()} (int: {discriminator_int})")
logger.info(f"🎯 Expected CreateEvent discriminator: {self._create_event_discriminator_bytes.hex()} (int: {self._create_event_discriminator})")
# Try to decode as CreateEvent using IDL parser
decoded_event = self._idl_parser.decode_event_data(decoded_data, "CreateEvent")
if not decoded_event:
logger.info("❌ IDL parser returned None for CreateEvent")
continue
if decoded_event.get('event_name') != 'CreateEvent':
logger.info(f"❌ Wrong event type: {decoded_event.get('event_name', 'None')}")
continue
logger.info(f"✅ Successfully decoded event: {decoded_event.get('event_name', 'Unknown')}")
logger.info(f"🔍 Event fields: {list(decoded_event.get('fields', {}).keys())}")
fields = decoded_event.get('fields', {})
if not fields:
logger.info("❌ No fields found in decoded event")
continue
# Validate required fields exist
required_fields = ["mint", "bonding_curve", "user", "creator", "name", "symbol", "uri"]
missing_fields = [field for field in required_fields if field not in fields]
if missing_fields:
logger.info(f"❌ Missing required fields: {missing_fields}")
continue
logger.info(f"🎯 Token found: {fields.get('symbol', 'Unknown')} ({fields.get('name', 'Unknown')})")
# Convert string representations to Pubkey objects
# Note: IDL parser returns pubkeys as base58 strings
try:
mint = Pubkey.from_string(fields["mint"]) if isinstance(fields["mint"], str) else fields["mint"]
bonding_curve = Pubkey.from_string(fields["bonding_curve"]) if isinstance(fields["bonding_curve"], str) else fields["bonding_curve"]
user = Pubkey.from_string(fields["user"]) if isinstance(fields["user"], str) else fields["user"]
creator = Pubkey.from_string(fields["creator"]) if isinstance(fields["creator"], str) else fields["creator"]
logger.info(f"🔑 Mint: {mint}")
logger.info(f"🔑 Bonding Curve: {bonding_curve}")
logger.info(f"🔑 User: {user}")
logger.info(f"🔑 Creator: {creator}")
except Exception as e:
logger.info(f"❌ Failed to convert pubkey fields: {e}")
continue
# Derive additional addresses
associated_bonding_curve = self._derive_associated_bonding_curve(mint, bonding_curve)
creator_vault = self._derive_creator_vault(creator)
logger.info(f"✅ Successfully parsed CreateEvent for token: {fields.get('symbol', 'Unknown')}")
return TokenInfo(
name=fields["name"],
symbol=fields["symbol"],
uri=fields["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 as e:
logger.info(f"❌ Failed to decode Program data entry {entry_idx + 1}: {e}")
continue
logger.info("❌ No valid CreateEvent found in any Program data entries")
return None
except Exception:
logger.exception("Failed to parse token creation from logs")
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 using injected IDL parser.
Args:
instruction_data: Raw instruction data
accounts: List of account indices
account_keys: List of account public keys
Returns:
TokenInfo if token creation found, None otherwise
"""
if not instruction_data.startswith(self._create_instruction_discriminator_bytes):
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 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
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)
user = get_account_key(7)
if not all([mint, bonding_curve, associated_bonding_curve, user]):
return None
# Create creator vault
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=args.get("name", ""),
symbol=args.get("symbol", ""),
uri=args.get("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 as e:
logger.debug(f"Failed to parse create instruction: {e}")
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
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 as e:
logger.debug(f"Failed to parse geyser transaction: {e}")
return None
def get_program_id(self) -> Pubkey:
"""Get the pump.fun program ID this parser monitors.
Returns:
Pump.fun program ID
"""
return PumpFunAddresses.PROGRAM
def get_instruction_discriminators(self) -> list[bytes]:
"""Get instruction discriminators for token creation.
Returns:
List of discriminator bytes to match
"""
return [self._create_instruction_discriminator_bytes]
def get_event_discriminators(self) -> list[bytes]:
"""Get event discriminators for token creation.
Returns:
List of event discriminator bytes to match
"""
return [self._create_event_discriminator_bytes]
def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
"""Parse token creation from block data (for block listener).
Args:
block_data: Block data from WebSocket
Returns:
TokenInfo if token creation found, None otherwise
"""
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 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("<Q", ix_data[:8])[0]
if discriminator == self._create_instruction_discriminator:
# 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, account_keys_bytes
)
if token_info:
return token_info
except Exception as e:
logger.debug(f"Failed to parse block transaction: {e}")
continue
# Handle already decoded transaction data
elif isinstance(tx_data, dict) and "message" in tx_data:
try:
message = tx_data["message"]
if "instructions" not in message or "accountKeys" not in message:
continue
for ix in message["instructions"]:
if "programIdIndex" not in ix or "accounts" not in ix or "data" not in ix:
continue
program_idx = ix["programIdIndex"]
if program_idx >= 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("<Q", ix_data[:8])[0]
if discriminator == self._create_instruction_discriminator:
if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
continue
# Convert account keys to bytes for parsing
account_keys_bytes = [
Pubkey.from_string(key).to_bytes()
for key in message["accountKeys"]
]
token_info = self.parse_token_creation_from_instruction(
ix_data, ix["accounts"], account_keys_bytes
)
if token_info:
return token_info
except Exception as e:
logger.debug(f"Failed to parse decoded block transaction: {e}")
continue
return None
except Exception as e:
logger.debug(f"Failed to parse block data: {e}")
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)],
PumpFunAddresses.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
@property
def verbose(self) -> 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
@@ -0,0 +1,248 @@
"""
Pump.Fun implementation of InstructionBuilder interface.
This module builds pump.fun-specific buy and sell instructions
by implementing the InstructionBuilder interface with IDL-based discriminators.
"""
import struct
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
from utils.idl_parser import IDLParser
from utils.logger import get_logger
logger = get_logger(__name__)
class PumpFunInstructionBuilder(InstructionBuilder):
"""Pump.Fun implementation of InstructionBuilder interface with IDL-based discriminators."""
def __init__(self, idl_parser: IDLParser):
"""Initialize pump.fun instruction builder with injected IDL parser.
Args:
idl_parser: Pre-loaded IDL parser for pump.fun platform
"""
self._idl_parser = idl_parser
# Get discriminators from injected IDL parser
discriminators = self._idl_parser.get_instruction_discriminators()
self._buy_discriminator = discriminators["buy"]
self._sell_discriminator = discriminators["sell"]
logger.info("Pump.Fun instruction builder initialized with injected IDL parser")
@property
def platform(self) -> Platform:
"""Get the platform this builder serves."""
return Platform.PUMP_FUN
async def build_buy_instruction(
self,
token_info: TokenInfo,
user: Pubkey,
amount_in: int,
minimum_amount_out: int,
address_provider: AddressProvider
) -> 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 = (
self._buy_discriminator +
struct.pack("<Q", minimum_amount_out) + # token amount in raw units
struct.pack("<Q", amount_in) # max SOL cost in lamports
)
buy_instruction = Instruction(
program_id=accounts_info["program"],
data=instruction_data,
accounts=buy_accounts
)
instructions.append(buy_instruction)
return instructions
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 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 = (
self._sell_discriminator +
struct.pack("<Q", amount_in) + # token amount in raw units
struct.pack("<Q", minimum_amount_out) # min SOL output in lamports
)
sell_instruction = Instruction(
program_id=accounts_info["program"],
data=instruction_data,
accounts=sell_accounts
)
instructions.append(sell_instruction)
return instructions
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
"""
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
@@ -1,33 +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 PumpAddresses, SystemAddresses
from trading.base import TokenInfo
from interfaces.core import Platform, TokenInfo
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):
"""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
token_data: Token data from PumpPortal
Returns:
True if this processor can handle the token data
"""
self.pump_program = pump_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
"""
@@ -40,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]):
@@ -60,17 +80,18 @@ 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,
symbol=symbol,
uri=uri,
mint=mint,
platform=Platform.PUMP_FUN,
bonding_curve=bonding_curve,
associated_bonding_curve=associated_bonding_curve,
user=user,
@@ -78,46 +99,6 @@ class PumpPortalEventProcessor:
creator_vault=creator_vault,
)
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)],
PumpAddresses.PROGRAM,
)
return derived_address
except Exception:
logger.exception("Failed to process PumpPortal token data")
return None
+325 -37
View File
@@ -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,79 @@ from typing import Any
from solders.pubkey import Pubkey
from core.pubkeys import PumpAddresses
# Import from interfaces to avoid duplication
from interfaces.core import Platform, TokenInfo
@dataclass
class TokenInfo:
"""Token information."""
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."""
name: str
symbol: str
uri: str
@@ -26,14 +96,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"],
@@ -66,42 +136,260 @@ class TokenInfo:
}
@dataclass
class TradeResult:
"""Result of a trading operation."""
success: bool
tx_signature: str | None = None
error_message: str | None = None
amount: float | None = None
price: float | None = None
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,
)
class Trader(ABC):
"""Base interface for trading operations."""
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,
)
@abstractmethod
async def execute(self, *args, **kwargs) -> TradeResult:
"""Execute trading operation.
Returns:
TradeResult with operation outcome
"""
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
"""
# 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,
symbol=symbol,
uri=uri,
mint=mint,
platform=Platform.PUMP_FUN,
bonding_curve=bonding_curve,
associated_bonding_curve=associated_bonding_curve,
user=user,
creator=creator or user,
creator_vault=creator_vault,
**kwargs
)
def _get_relevant_accounts(self, token_info: TokenInfo) -> list[Pubkey]:
"""
Get the list of accounts relevant for calculating the priority fee.
Args:
token_info: Token information for the buy/sell operation.
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
"""
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 or user,
**kwargs
)
Returns:
list[Pubkey]: List of relevant accounts.
"""
return [
token_info.mint, # Token mint address
token_info.bonding_curve, # Bonding curve address
PumpAddresses.PROGRAM, # Pump.fun program address
PumpAddresses.FEE, # Pump.fun fee account
]
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
__all__ = [
'Platform', # Platform enum
'TokenInfo', # Enhanced TokenInfo (main export)
'TokenInfo_Legacy', # Legacy TokenInfo for compatibility
'TradeResult', # Enhanced TradeResult
'Trader', # Enhanced Trader base class
'create_legacy_token_info',
'create_lets_bonk_token_info',
'create_pump_fun_token_info',
'get_platform_specific_fields',
'is_lets_bonk_token',
'is_pump_fun_token',
'upgrade_token_info',
'validate_token_info',
]
-416
View File
@@ -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("<Q", 16927863322537952870)
class TokenBuyer(Trader):
"""Handles buying tokens on pump.fun."""
def __init__(
self,
client: SolanaClient,
wallet: Wallet,
curve_manager: BondingCurveManager,
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 token buyer.
Args:
client: Solana client for RPC calls
wallet: Wallet for signing transactions
curve_manager: Bonding curve manager
amount: Amount of SOL to spend
slippage: Slippage tolerance (0.01 = 1%)
max_retries: Maximum number of retry attempts
extreme_fast_token_amount: Amount of token to buy if extreme fast mode is enabled
extreme_fast_mode: If enabled, avoid fetching associated bonding curve state
"""
self.client = client
self.wallet = wallet
self.curve_manager = curve_manager
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.
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("<Q", token_amount_raw)
+ struct.pack("<Q", max_amount_lamports)
)
buy_ix = Instruction(PumpAddresses.PROGRAM, data, accounts)
try:
return await self.client.build_and_send_transaction(
[idempotent_ata_ix, buy_ix],
self.wallet.keypair,
skip_preflight=True,
max_retries=self.max_retries,
priority_fee=await self.priority_fee_manager.calculate_priority_fee(
self._get_relevant_accounts(token_info)
),
)
except Exception as e:
logger.error(f"Buy transaction failed: {e!s}")
raise
async def _get_actual_execution_price(
self, tx_signature: str, token_info: TokenInfo
) -> 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
+270
View File
@@ -0,0 +1,270 @@
"""
Platform-aware trader implementations that use the interface system.
Final cleanup removing all platform-specific hardcoding.
"""
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."""
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) -> TradeResult:
"""Execute buy operation using platform-specific implementations."""
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 using platform-agnostic method
pool_address = self._get_pool_address(token_info, address_provider)
# Regular behavior with RPC call
token_price_sol = await curve_manager.calculate_price(pool_address)
token_amount = self.amount / token_price_sol if token_price_sol > 0 else 0
# 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 using platform-specific builder
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 on {token_info.platform.value}"
)
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.exception("Buy operation failed")
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 using platform-agnostic method."""
# Try to get the address from token_info first, then derive if needed
if token_info.platform == Platform.PUMP_FUN:
if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve:
return token_info.bonding_curve
elif token_info.platform == Platform.LETS_BONK:
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):
"""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."""
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) -> TradeResult:
"""Execute sell operation using platform-specific implementations."""
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 using platform-agnostic method
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 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 using platform-specific builder
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.exception("Sell operation failed")
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 using platform-agnostic method."""
# Try to get the address from token_info first, then derive if needed
if token_info.platform == Platform.PUMP_FUN:
if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve:
return token_info.bonding_curve
elif token_info.platform == Platform.LETS_BONK:
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)
-213
View File
@@ -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("<Q", 12502976635542562355)
class TokenSeller(Trader):
"""Handles selling tokens on pump.fun."""
def __init__(
self,
client: SolanaClient,
wallet: Wallet,
curve_manager: BondingCurveManager,
priority_fee_manager: PriorityFeeManager,
slippage: float = 0.25,
max_retries: int = 5,
):
"""Initialize token seller.
Args:
client: Solana client for RPC calls
wallet: Wallet for signing transactions
curve_manager: Bonding curve manager
slippage: Slippage tolerance (0.25 = 25%)
max_retries: Maximum number of retry attempts
"""
self.client = client
self.wallet = wallet
self.curve_manager = curve_manager
self.priority_fee_manager = priority_fee_manager
self.slippage = slippage
self.max_retries = max_retries
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> 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("<Q", token_amount)
+ struct.pack("<Q", min_sol_output)
)
sell_ix = Instruction(PumpAddresses.PROGRAM, data, accounts)
try:
return await self.client.build_and_send_transaction(
[sell_ix],
self.wallet.keypair,
skip_preflight=True,
max_retries=self.max_retries,
priority_fee=await self.priority_fee_manager.calculate_priority_fee(
self._get_relevant_accounts(token_info)
),
)
except Exception as e:
logger.error(f"Sell transaction failed: {e!s}")
raise
@@ -1,12 +1,12 @@
"""
Main trading coordinator for pump.fun tokens.
Refactored PumpTrader to only process fresh tokens from WebSocket.
Universal trading coordinator that works with any platform.
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
@@ -18,18 +18,14 @@ from cleanup.modes import (
handle_cleanup_post_session,
)
from core.client import SolanaClient
from core.curve import BondingCurveManager
from core.priority_fee.manager import PriorityFeeManager
from core.pubkeys import PumpAddresses
from core.wallet import Wallet
from monitoring.block_listener import BlockListener
from monitoring.geyser_listener import GeyserListener
from monitoring.logs_listener import LogsListener
from monitoring.pumpportal_listener import PumpPortalListener
from trading.base import TokenInfo, TradeResult
from trading.buyer import TokenBuyer
from interfaces.core import Platform, TokenInfo
from monitoring.listener_factory import ListenerFactory
from platforms import get_platform_implementations
from trading.base import TradeResult
from trading.platform_aware import PlatformAwareBuyer, PlatformAwareSeller
from trading.position import Position
from trading.seller import TokenSeller
from utils.logger import get_logger
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
@@ -37,8 +33,8 @@ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
logger = get_logger(__name__)
class PumpTrader:
"""Coordinates trading operations for pump.fun tokens with focus on freshness."""
class UniversalTrader:
"""Universal trading coordinator that works with any supported platform."""
def __init__(
self,
@@ -48,11 +44,15 @@ class PumpTrader:
buy_amount: float,
buy_slippage: float,
sell_slippage: float,
# Platform configuration
platform: Platform | str = Platform.PUMP_FUN,
# Listener configuration
listener_type: str = "logs",
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",
# Trading configuration
extreme_fast_mode: bool = False,
extreme_fast_token_amount: int = 30,
# Exit strategy configuration
@@ -69,7 +69,7 @@ class PumpTrader:
hard_cap_prior_fee: int = 200_000,
# Retry and timeout settings
max_retries: int = 3,
wait_time_after_creation: int = 15, # here and further - seconds
wait_time_after_creation: int = 15,
wait_time_after_buy: int = 15,
wait_time_before_new_token: int = 15,
max_token_age: int | float = 0.001,
@@ -84,55 +84,10 @@ class PumpTrader:
marry_mode: bool = False,
yolo_mode: bool = False,
):
"""Initialize the pump 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
listener_type: Type of listener to use ('logs', 'blocks', 'geyser', or 'pumpportal')
geyser_endpoint: Geyser endpoint URL (required for geyser listener)
geyser_api_token: Geyser API token (required for geyser listener)
geyser_auth_type: Geyser authentication type ('x-token' or 'basic')
pumpportal_url: PumpPortal WebSocket URL (default: wss://pumpportal.fun/api/data)
extreme_fast_mode: Whether to enable extreme fast mode
extreme_fast_token_amount: Maximum token amount for extreme fast mode
exit_strategy: Exit strategy ("time_based", "tp_sl", or "manual")
take_profit_percentage: Take profit percentage (0.5 = 50% profit)
stop_loss_percentage: Stop loss percentage (0.2 = 20% loss)
max_hold_time: Maximum hold time in seconds
price_check_interval: How often to check price for TP/SL (seconds)
enable_dynamic_priority_fee: Whether to enable dynamic priority fees
enable_fixed_priority_fee: Whether to enable fixed priority fees
fixed_priority_fee: Fixed priority fee amount
extra_priority_fee: Extra percentage for priority fees
hard_cap_prior_fee: Hard cap for priority fees
max_retries: Maximum number of retry attempts
wait_time_after_creation: Time to wait after token creation (seconds)
wait_time_after_buy: Time to wait after buying a token (seconds)
wait_time_before_new_token: Time to wait before processing a new token (seconds)
max_token_age: Maximum age of token to process (seconds)
token_wait_timeout: Timeout for waiting for a token in single-token mode (seconds)
cleanup_mode: Cleanup mode ("disabled", "auto", or "manual")
cleanup_force_close_with_burn: Whether to force close with burn during cleanup
cleanup_with_priority_fee: Whether to use priority fees during cleanup
match_string: Optional string to match in token name/symbol
bro_address: Optional creator address to filter by
marry_mode: If True, only buy tokens and skip selling
yolo_mode: If True, trade continuously
"""
"""Initialize the universal trader."""
# Core components
self.solana_client = SolanaClient(rpc_endpoint)
self.wallet = Wallet(private_key)
self.curve_manager = BondingCurveManager(self.solana_client)
self.priority_fee_manager = PriorityFeeManager(
client=self.solana_client,
enable_dynamic_fee=enable_dynamic_priority_fee,
@@ -141,10 +96,33 @@ class PumpTrader:
extra_fee=extra_priority_fee,
hard_cap=hard_cap_prior_fee,
)
self.buyer = TokenBuyer(
# Platform setup
if isinstance(platform, str):
self.platform = Platform(platform)
else:
self.platform = platform
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:
logger.exception("Platform validation failed")
raise
# Get platform-specific implementations
self.platform_implementations = get_platform_implementations(
self.platform, self.solana_client
)
# Create platform-aware traders
self.buyer = PlatformAwareBuyer(
self.solana_client,
self.wallet,
self.curve_manager,
self.priority_fee_manager,
buy_amount,
buy_slippage,
@@ -152,41 +130,25 @@ class PumpTrader:
extreme_fast_token_amount,
extreme_fast_mode,
)
self.seller = TokenSeller(
self.seller = PlatformAwareSeller(
self.solana_client,
self.wallet,
self.curve_manager,
self.priority_fee_manager,
sell_slippage,
max_retries,
)
# Initialize the appropriate listener type
listener_type = listener_type.lower()
if listener_type == "geyser":
if not geyser_endpoint or not geyser_api_token:
raise ValueError(
"Geyser endpoint and API token are required for geyser listener"
)
self.token_listener = GeyserListener(
geyser_endpoint,
geyser_api_token,
geyser_auth_type,
PumpAddresses.PROGRAM,
)
logger.info("Using Geyser listener for token monitoring")
elif listener_type == "logs":
self.token_listener = LogsListener(wss_endpoint, PumpAddresses.PROGRAM)
logger.info("Using logsSubscribe listener for token monitoring")
elif listener_type == "pumpportal":
self.token_listener = PumpPortalListener(
PumpAddresses.PROGRAM, pumpportal_url
)
logger.info("Using PumpPortal listener for token monitoring")
else:
self.token_listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM)
logger.info("Using blockSubscribe listener for token monitoring")
# Initialize the appropriate listener with platform filtering
self.token_listener = ListenerFactory.create_listener(
listener_type=listener_type,
wss_endpoint=wss_endpoint,
geyser_endpoint=geyser_endpoint,
geyser_api_token=geyser_api_token,
geyser_auth_type=geyser_auth_type,
pumpportal_url=pumpportal_url,
platforms=[self.platform], # Only listen for our platform
)
# Trading parameters
self.buy_amount = buy_amount
@@ -230,26 +192,18 @@ class PumpTrader:
async def start(self) -> None:
"""Start the trading bot and listen for new tokens."""
logger.info("Starting pump.fun trader")
logger.info(
f"Match filter: {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 +216,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:
@@ -286,8 +234,8 @@ class PumpTrader:
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:
@@ -295,19 +243,15 @@ class PumpTrader:
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()
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.
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
@@ -334,16 +278,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()
@@ -366,8 +306,8 @@ class PumpTrader:
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:
@@ -376,11 +316,7 @@ class PumpTrader:
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:
@@ -391,7 +327,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,52 +338,41 @@ 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:
# Handle cancellation gracefully
logger.info("Token queue processor was cancelled")
break
except Exception as e:
logger.error(f"Error in token queue processor: {e!s}")
except Exception:
logger.exception("Error in token queue processor")
finally:
self.token_queue.task_done()
async def _handle_token(self, token_info: TokenInfo) -> None:
"""Handle a new token creation event.
Args:
token_info: Token information
"""
"""Handle a new token creation event."""
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 +382,16 @@ 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}")
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.
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,
)
async def _handle_successful_buy(self, token_info: TokenInfo, buy_result: TradeResult) -> None:
"""Handle successful token purchase."""
logger.info(f"Successfully bought {token_info.symbol} on {token_info.platform.value}")
self._log_trade("buy", token_info, buy_result.price, buy_result.amount, buy_result.tx_signature)
self.traded_mints.add(token_info.mint)
# Choose exit strategy
@@ -495,15 +405,8 @@ class PumpTrader:
else:
logger.info("Marry mode enabled. Skipping sell operation.")
async def _handle_failed_buy(
self, token_info: TokenInfo, buy_result: TradeResult
) -> None:
"""Handle failed token purchase.
Args:
token_info: Token information
buy_result: The result of the buy operation
"""
async def _handle_failed_buy(self, token_info: TokenInfo, buy_result: TradeResult) -> None:
"""Handle failed token purchase."""
logger.error(f"Failed to buy {token_info.symbol}: {buy_result.error_message}")
# Close ATA if enabled
await handle_cleanup_after_failure(
@@ -516,21 +419,14 @@ class PumpTrader:
self.cleanup_force_close_with_burn,
)
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
"""
async def _handle_tp_sl_exit(self, token_info: TokenInfo, buy_result: TradeResult) -> None:
"""Handle take profit/stop loss exit strategy."""
# Create position
position = Position.create_from_buy_result(
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,
@@ -546,11 +442,7 @@ class PumpTrader:
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)
@@ -559,13 +451,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 +463,20 @@ 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:
"""Monitor a position until exit conditions are met.
async def _monitor_position_until_exit(self, token_info: TokenInfo, position: Position) -> None:
"""Monitor a position until exit conditions are met."""
logger.info(f"Starting position monitoring (check interval: {self.price_check_interval}s)")
Args:
token_info: Token information
position: Position to monitor
"""
logger.info(
f"Starting position monitoring (check interval: {self.price_check_interval}s)"
)
# Get pool address for price monitoring using platform-agnostic method
pool_address = self._get_pool_address(token_info)
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 +487,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,68 +514,84 @@ 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
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:
"""Get the pool/curve address for price monitoring using platform-agnostic method."""
address_provider = self.platform_implementations.address_provider
# Use platform-specific logic to get the appropriate address
if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve:
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 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")
trades_dir = Path("trades")
trades_dir.mkdir(exist_ok=True)
file_path = trades_dir / f"{token_info.mint}.txt"
with open(file_name, "w") as file:
file.write(json.dumps(token_info.to_dict(), indent=2))
# Convert to dictionary for saving - platform-agnostic
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,
"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)
logger.info(f"Token information saved to {file_name}")
except Exception as e:
logger.error(f"Failed to save token information: {e!s}")
file_path.write_text(json.dumps(token_dict, indent=2))
def _log_trade(
self,
action: str,
token_info: TokenInfo,
price: float,
amount: float,
tx_hash: str | None,
) -> None:
"""Log trade information.
logger.info(f"Token information saved to {file_path}")
except OSError:
logger.exception("Failed to save token 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
"""
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(),
"action": action,
"platform": token_info.platform.value,
"token_address": str(token_info.mint),
"symbol": token_info.symbol,
"price": price,
@@ -718,7 +599,12 @@ class PumpTrader:
"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
PumpTrader = UniversalTrader # Legacy name for backward compatibility
+347
View File
@@ -0,0 +1,347 @@
"""
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.
"""
from pathlib import Path
from typing import Any
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_file = Path(__file__)
project_root = current_file.parent.parent.parent
# Define IDL paths for each platform
self._idl_paths = {
Platform.LETS_BONK: project_root / "idl" / "raydium_launchlab_idl.json",
Platform.PUMP_FUN: project_root / "idl" / "pump_fun_idl.json",
}
def get_parser(self, platform: Platform, verbose: bool = False) -> IDLParser:
"""Get or create an IDL parser for the specified platform.
Args:
platform: Platform to get parser for
verbose: Whether to enable verbose logging in the parser
Returns:
IDLParser instance for the platform
Raises:
ValueError: If platform is not supported or IDL file not found
"""
# Return cached parser if available
if platform in self._parsers:
return self._parsers[platform]
# Check if platform has IDL support
if platform not in self._idl_paths:
raise ValueError(f"Platform {platform.value} does not have IDL support configured")
idl_path = self._idl_paths[platform]
# Verify IDL file exists
if not idl_path.exists():
raise FileNotFoundError(f"IDL file not found for {platform.value} at {idl_path}")
# Load and cache the parser
logger.info(f"Loading IDL parser for {platform.value} from {idl_path}")
parser = IDLParser(idl_path, verbose=verbose)
self._parsers[platform] = parser
instruction_count = len(parser.get_instruction_names())
event_count = len(parser.get_event_names())
logger.info(f"IDL parser loaded for {platform.value} with {instruction_count} instructions and {event_count} events")
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")
# --------------------------------------------------------------------------
# Instruction-related convenience methods
# --------------------------------------------------------------------------
def get_instruction_discriminators(self, platform: Platform) -> dict[str, bytes]:
"""Get instruction discriminators for a platform.
Args:
platform: Platform to get discriminators for
Returns:
Dictionary mapping instruction names to discriminator bytes
"""
parser = self.get_parser(platform)
return parser.get_instruction_discriminators()
def get_instruction_names(self, platform: Platform) -> list[str]:
"""Get available instruction names for a platform.
Args:
platform: Platform to get instruction names for
Returns:
List of instruction names
"""
parser = self.get_parser(platform)
return parser.get_instruction_names()
# --------------------------------------------------------------------------
# Event-related convenience methods
# --------------------------------------------------------------------------
def get_event_discriminators(self, platform: Platform) -> dict[str, bytes]:
"""Get event discriminators for a platform.
Args:
platform: Platform to get event discriminators for
Returns:
Dictionary mapping event names to discriminator bytes
"""
parser = self.get_parser(platform)
return parser.get_event_discriminators()
def get_event_names(self, platform: Platform) -> list[str]:
"""Get available event names for a platform.
Args:
platform: Platform to get event names for
Returns:
List of event names
"""
parser = self.get_parser(platform)
return parser.get_event_names()
def decode_event_from_logs(self, platform: Platform, logs: list[str], event_name: str | None = None) -> dict | None:
"""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.exception(f"Failed to get capabilities for {platform.value}")
return {
"platform": platform.value,
"has_idl_support": False,
"error": str(e),
"instructions": [],
"events": [],
"instruction_count": 0,
"event_count": 0,
}
# 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)
# --------------------------------------------------------------------------
# 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)
+504
View File
@@ -0,0 +1,504 @@
"""
IDL Parser module for Solana programs.
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
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 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.
_PRIMITIVE_TYPE_INFO = {
# type_name: (format_char, size_in_bytes)
'u8': ('<B', 1),
'u16': ('<H', 2),
'u32': ('<I', 4),
'u64': ('<Q', 8),
'i8': ('<b', 1),
'i16': ('<h', 2),
'i32': ('<i', 4),
'i64': ('<q', 8),
'bool': ('<?', 1),
'pubkey': (None, PUBLIC_KEY_SIZE),
'string': (None, STRING_LENGTH_PREFIX_SIZE), # Min size is for the length prefix
}
def __init__(self, idl_path: str, verbose: bool = False):
"""
Initialize the IDL parser.
Args:
idl_path: Path to the IDL JSON file
verbose: Whether to print debug information during initialization
"""
self.verbose = verbose
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) - Instructions
# --------------------------------------------------------------------------
def get_instruction_discriminators(self) -> 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
}
# --------------------------------------------------------------------------
# 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]
# 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
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_name_actual,
'fields': event_fields
}
except Exception as e:
if self.verbose:
print(f"❌ Error decoding event {event_name_actual}: {e}")
return None
def find_event_in_logs(self, logs: list[str], target_event_name: str | None = None) -> dict[str, Any] | None:
"""
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.
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_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', []):
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('<I', data, offset)[0]
offset += STRING_LENGTH_PREFIX_SIZE
value = data[offset:offset + length].decode('utf-8')
return value, offset + length
if type_name == 'pubkey':
end = offset + PUBLIC_KEY_SIZE
value = base58.b58encode(data[offset:end]).decode('utf-8')
return value, end
# Handle all numeric and bool types from the map
fmt, size = self._PRIMITIVE_TYPE_INFO[type_name]
value = struct.unpack_from(fmt, data, offset)[0]
return value, offset + size
def _decode_defined_type(self, data: bytes, offset: int, type_name: str) -> tuple[dict[str, Any], int]:
"""Decode user-defined types (structs and enums)."""
if type_name not in self.types:
raise ValueError(f"Unknown defined type: {type_name}")
type_def = self.types[type_name]['type']
if type_def['kind'] == 'struct':
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('<B', data, offset)[0]
offset += ENUM_DISCRIMINATOR_SIZE
variants = type_def['variants']
if variant_index >= 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)
-209
View File
@@ -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))
-102
View File
@@ -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))
-109
View File
@@ -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))
-102
View File
@@ -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))
+2
View File
@@ -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"}