mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-07-27 15:27:44 +00:00
wip(core): platform aware trading
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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}"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -11,6 +11,9 @@ private_key: "${SOLANA_PRIVATE_KEY}"
|
||||
enabled: false # You can turn off the bot w/o removing its config
|
||||
separate_process: true
|
||||
|
||||
# Options: "pump_fun" (default), "lets_bonk"
|
||||
platform: "pump_fun"
|
||||
|
||||
# PumpPortal configuration (optional - uses default URL if not specified)
|
||||
pumpportal:
|
||||
url: "wss://pumpportal.fun/api/data" # Default PumpPortal WebSocket URL
|
||||
|
||||
+80
-1
@@ -1,9 +1,19 @@
|
||||
"""
|
||||
Updated configuration validation with platform support.
|
||||
|
||||
This extends the existing config_loader.py to support platform selection
|
||||
and validation while maintaining backward compatibility.
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from interfaces.core import Platform
|
||||
|
||||
# Existing validation rules (keeping all existing ones)
|
||||
REQUIRED_FIELDS = [
|
||||
"name",
|
||||
"rpc_endpoint",
|
||||
@@ -64,17 +74,19 @@ CONFIG_VALIDATION_RULES = [
|
||||
),
|
||||
]
|
||||
|
||||
# Valid values for enum-like fields
|
||||
# Valid values for enum-like fields (extended with platform support)
|
||||
VALID_VALUES = {
|
||||
"filters.listener_type": ["logs", "blocks", "geyser", "pumpportal"],
|
||||
"cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"],
|
||||
"trade.exit_strategy": ["time_based", "tp_sl", "manual"],
|
||||
"platform": ["pump_fun", "lets_bonk"], # New platform validation
|
||||
}
|
||||
|
||||
|
||||
def load_bot_config(path: str) -> dict:
|
||||
"""
|
||||
Load and validate a bot configuration from a YAML file.
|
||||
Extended to support platform selection with backward compatibility.
|
||||
|
||||
Args:
|
||||
path: Path to the YAML configuration file (relative or absolute)
|
||||
@@ -99,6 +111,11 @@ def load_bot_config(path: str) -> dict:
|
||||
load_dotenv(env_file, override=True)
|
||||
|
||||
resolve_env_vars(config)
|
||||
|
||||
# Set default platform if not specified (backward compatibility)
|
||||
if "platform" not in config:
|
||||
config["platform"] = "pump_fun"
|
||||
|
||||
validate_config(config)
|
||||
|
||||
return config
|
||||
@@ -157,6 +174,7 @@ def get_nested_value(config: dict, path: str) -> Any:
|
||||
def validate_config(config: dict) -> None:
|
||||
"""
|
||||
Validate the configuration against defined rules.
|
||||
Extended to include platform-specific validation.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary to validate
|
||||
@@ -164,9 +182,11 @@ def validate_config(config: dict) -> None:
|
||||
Raises:
|
||||
ValueError: If the configuration is invalid
|
||||
"""
|
||||
# Validate required fields
|
||||
for field in REQUIRED_FIELDS:
|
||||
get_nested_value(config, field)
|
||||
|
||||
# Validate config rules
|
||||
for path, expected_type, min_val, max_val, error_msg in CONFIG_VALIDATION_RULES:
|
||||
try:
|
||||
value = get_nested_value(config, path)
|
||||
@@ -206,15 +226,70 @@ def validate_config(config: dict) -> None:
|
||||
# Skip if one of the fields is missing
|
||||
pass
|
||||
|
||||
# Platform-specific validation
|
||||
platform_str = config.get("platform", "pump_fun")
|
||||
try:
|
||||
platform = Platform(platform_str)
|
||||
validate_platform_config(config, platform)
|
||||
except ValueError as e:
|
||||
if "is not a valid" in str(e):
|
||||
raise ValueError(f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}")
|
||||
raise
|
||||
|
||||
|
||||
def validate_platform_config(config: dict, platform: Platform) -> None:
|
||||
"""
|
||||
Validate platform-specific configuration requirements.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary
|
||||
platform: Platform enum value
|
||||
|
||||
Raises:
|
||||
ValueError: If platform-specific config is invalid
|
||||
"""
|
||||
if platform == Platform.PUMP_FUN:
|
||||
# pump.fun doesn't require additional config beyond base requirements
|
||||
pass
|
||||
|
||||
elif platform == Platform.LETS_BONK:
|
||||
# LetsBonk may require additional configuration in the future
|
||||
# For now, it uses the same base configuration as pump.fun
|
||||
pass
|
||||
|
||||
|
||||
def get_platform_from_config(config: dict) -> Platform:
|
||||
"""
|
||||
Extract platform enum from configuration.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary
|
||||
|
||||
Returns:
|
||||
Platform enum value
|
||||
|
||||
Raises:
|
||||
ValueError: If platform is invalid
|
||||
"""
|
||||
platform_str = config.get("platform", "pump_fun")
|
||||
try:
|
||||
return Platform(platform_str)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}")
|
||||
|
||||
|
||||
def print_config_summary(config: dict) -> None:
|
||||
"""
|
||||
Print a summary of the loaded configuration.
|
||||
Extended to show platform information.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary
|
||||
"""
|
||||
platform_str = config.get("platform", "pump_fun")
|
||||
|
||||
print(f"Bot name: {config.get('name', 'unnamed')}")
|
||||
print(f"Platform: {platform_str}")
|
||||
print(
|
||||
f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}"
|
||||
)
|
||||
@@ -240,5 +315,9 @@ def print_config_summary(config: dict) -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage with platform configuration
|
||||
config = load_bot_config("bots/bot-sniper.yaml")
|
||||
print_config_summary(config)
|
||||
|
||||
platform = get_platform_from_config(config)
|
||||
print(f"Detected platform: {platform}")
|
||||
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
Platform factory and registry for managing multiple trading platforms.
|
||||
|
||||
This module provides a centralized way to instantiate and access
|
||||
platform-specific implementations of the trading interfaces.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
from core.client import SolanaClient
|
||||
from interfaces.core import (
|
||||
AddressProvider,
|
||||
CurveManager,
|
||||
EventParser,
|
||||
InstructionBuilder,
|
||||
Platform,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlatformImplementations:
|
||||
"""Container for all platform-specific implementations."""
|
||||
address_provider: AddressProvider
|
||||
instruction_builder: InstructionBuilder
|
||||
curve_manager: CurveManager
|
||||
event_parser: EventParser
|
||||
|
||||
|
||||
class PlatformRegistry:
|
||||
"""Registry for platform implementations."""
|
||||
|
||||
def __init__(self):
|
||||
self._implementations: dict[Platform, dict[str, type]] = {}
|
||||
self._instances: dict[Platform, PlatformImplementations] = {}
|
||||
|
||||
def register_platform(
|
||||
self,
|
||||
platform: Platform,
|
||||
address_provider_class: type[AddressProvider],
|
||||
instruction_builder_class: type[InstructionBuilder],
|
||||
curve_manager_class: type[CurveManager],
|
||||
event_parser_class: type[EventParser]
|
||||
) -> None:
|
||||
"""Register platform implementations.
|
||||
|
||||
Args:
|
||||
platform: Platform enum value
|
||||
address_provider_class: AddressProvider implementation class
|
||||
instruction_builder_class: InstructionBuilder implementation class
|
||||
curve_manager_class: CurveManager implementation class
|
||||
event_parser_class: EventParser implementation class
|
||||
"""
|
||||
self._implementations[platform] = {
|
||||
'address_provider': address_provider_class,
|
||||
'instruction_builder': instruction_builder_class,
|
||||
'curve_manager': curve_manager_class,
|
||||
'event_parser': event_parser_class
|
||||
}
|
||||
|
||||
def create_platform_implementations(
|
||||
self,
|
||||
platform: Platform,
|
||||
client: SolanaClient,
|
||||
**kwargs: Any
|
||||
) -> PlatformImplementations:
|
||||
"""Create platform implementation instances.
|
||||
|
||||
Args:
|
||||
platform: Platform to create implementations for
|
||||
client: Solana RPC client
|
||||
**kwargs: Additional arguments for implementation constructors
|
||||
|
||||
Returns:
|
||||
PlatformImplementations containing all interface implementations
|
||||
|
||||
Raises:
|
||||
ValueError: If platform is not registered
|
||||
"""
|
||||
if platform not in self._implementations:
|
||||
raise ValueError(f"Platform {platform} is not registered")
|
||||
|
||||
# Check if we already have instances for this platform
|
||||
if platform in self._instances:
|
||||
return self._instances[platform]
|
||||
|
||||
impl_classes = self._implementations[platform]
|
||||
|
||||
# Create instances
|
||||
address_provider = impl_classes['address_provider']()
|
||||
instruction_builder = impl_classes['instruction_builder']()
|
||||
curve_manager = impl_classes['curve_manager'](client)
|
||||
event_parser = impl_classes['event_parser']()
|
||||
|
||||
implementations = PlatformImplementations(
|
||||
address_provider=address_provider,
|
||||
instruction_builder=instruction_builder,
|
||||
curve_manager=curve_manager,
|
||||
event_parser=event_parser
|
||||
)
|
||||
|
||||
# Cache the instances
|
||||
self._instances[platform] = implementations
|
||||
|
||||
return implementations
|
||||
|
||||
def get_platform_implementations(self, platform: Platform) -> PlatformImplementations | None:
|
||||
"""Get cached platform implementations.
|
||||
|
||||
Args:
|
||||
platform: Platform to get implementations for
|
||||
|
||||
Returns:
|
||||
PlatformImplementations if available, None otherwise
|
||||
"""
|
||||
return self._instances.get(platform)
|
||||
|
||||
def get_supported_platforms(self) -> list[Platform]:
|
||||
"""Get list of supported platforms.
|
||||
|
||||
Returns:
|
||||
List of registered platforms
|
||||
"""
|
||||
return list(self._implementations.keys())
|
||||
|
||||
def is_platform_supported(self, platform: Platform) -> bool:
|
||||
"""Check if a platform is supported.
|
||||
|
||||
Args:
|
||||
platform: Platform to check
|
||||
|
||||
Returns:
|
||||
True if platform is registered, False otherwise
|
||||
"""
|
||||
return platform in self._implementations
|
||||
|
||||
|
||||
class PlatformFactory:
|
||||
"""Factory for creating platform-specific implementations."""
|
||||
|
||||
def __init__(self):
|
||||
self.registry = PlatformRegistry()
|
||||
self._setup_default_platforms()
|
||||
|
||||
def _setup_default_platforms(self) -> None:
|
||||
"""Setup default platform registrations.
|
||||
|
||||
This will be populated as we implement each platform.
|
||||
"""
|
||||
# Platforms will be registered here as they are implemented
|
||||
# Example:
|
||||
# from platforms.pumpfun import PumpFunAddressProvider, PumpFunInstructionBuilder, ...
|
||||
# self.registry.register_platform(
|
||||
# Platform.PUMP_FUN,
|
||||
# PumpFunAddressProvider,
|
||||
# PumpFunInstructionBuilder,
|
||||
# PumpFunCurveManager,
|
||||
# PumpFunEventParser
|
||||
# )
|
||||
pass
|
||||
|
||||
def create_for_platform(
|
||||
self,
|
||||
platform: Platform,
|
||||
client: SolanaClient,
|
||||
**config: Any
|
||||
) -> PlatformImplementations:
|
||||
"""Create all implementations for a specific platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to create implementations for
|
||||
client: Solana RPC client
|
||||
**config: Platform-specific configuration
|
||||
|
||||
Returns:
|
||||
PlatformImplementations containing all interface implementations
|
||||
"""
|
||||
return self.registry.create_platform_implementations(platform, client, **config)
|
||||
|
||||
def get_address_provider(self, platform: Platform, client: SolanaClient) -> AddressProvider:
|
||||
"""Get address provider for platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to get provider for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
AddressProvider implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
return implementations.address_provider
|
||||
|
||||
def get_instruction_builder(self, platform: Platform, client: SolanaClient) -> InstructionBuilder:
|
||||
"""Get instruction builder for platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to get builder for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
InstructionBuilder implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
return implementations.instruction_builder
|
||||
|
||||
def get_curve_manager(self, platform: Platform, client: SolanaClient) -> CurveManager:
|
||||
"""Get curve manager for platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to get manager for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
CurveManager implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
return implementations.curve_manager
|
||||
|
||||
def get_event_parser(self, platform: Platform, client: SolanaClient) -> EventParser:
|
||||
"""Get event parser for platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to get parser for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
EventParser implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
return implementations.event_parser
|
||||
|
||||
def get_supported_platforms(self) -> list[Platform]:
|
||||
"""Get list of supported platforms.
|
||||
|
||||
Returns:
|
||||
List of supported platforms
|
||||
"""
|
||||
return self.registry.get_supported_platforms()
|
||||
|
||||
|
||||
# Global factory instance
|
||||
platform_factory = PlatformFactory()
|
||||
|
||||
|
||||
def get_platform_implementations(platform: Platform, client: SolanaClient) -> PlatformImplementations:
|
||||
"""Convenience function to get platform implementations.
|
||||
|
||||
Args:
|
||||
platform: Platform to get implementations for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
PlatformImplementations containing all interface implementations
|
||||
"""
|
||||
return platform_factory.create_for_platform(platform, client)
|
||||
|
||||
|
||||
def register_platform_implementations(
|
||||
platform: Platform,
|
||||
address_provider_class: type[AddressProvider],
|
||||
instruction_builder_class: type[InstructionBuilder],
|
||||
curve_manager_class: type[CurveManager],
|
||||
event_parser_class: type[EventParser]
|
||||
) -> None:
|
||||
"""Register platform implementations with the global factory.
|
||||
|
||||
Args:
|
||||
platform: Platform enum value
|
||||
address_provider_class: AddressProvider implementation class
|
||||
instruction_builder_class: InstructionBuilder implementation class
|
||||
curve_manager_class: CurveManager implementation class
|
||||
event_parser_class: EventParser implementation class
|
||||
"""
|
||||
platform_factory.registry.register_platform(
|
||||
platform,
|
||||
address_provider_class,
|
||||
instruction_builder_class,
|
||||
curve_manager_class,
|
||||
event_parser_class
|
||||
)
|
||||
@@ -0,0 +1,363 @@
|
||||
"""
|
||||
Core interfaces for multi-platform trading bot architecture.
|
||||
|
||||
This module defines the abstract base classes that each trading platform
|
||||
must implement to enable unified trading operations across different protocols.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
from solders.instruction import Instruction
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
|
||||
class Platform(Enum):
|
||||
"""Supported trading platforms."""
|
||||
PUMP_FUN = "pump_fun"
|
||||
LETS_BONK = "lets_bonk"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TokenInfo:
|
||||
"""Enhanced token information with platform support."""
|
||||
# Core token data
|
||||
name: str
|
||||
symbol: str
|
||||
uri: str
|
||||
mint: Pubkey
|
||||
|
||||
# Platform-specific fields
|
||||
platform: Platform
|
||||
bonding_curve: Pubkey | None = None # pump.fun specific
|
||||
associated_bonding_curve: Pubkey | None = None # pump.fun specific
|
||||
pool_state: Pubkey | None = None # LetsBonk specific
|
||||
base_vault: Pubkey | None = None # LetsBonk specific
|
||||
quote_vault: Pubkey | None = None # LetsBonk specific
|
||||
|
||||
# Common fields
|
||||
user: Pubkey | None = None
|
||||
creator: Pubkey | None = None
|
||||
creator_vault: Pubkey | None = None
|
||||
|
||||
# Metadata
|
||||
creation_timestamp: float | None = None
|
||||
additional_data: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class AddressProvider(ABC):
|
||||
"""Abstract interface for platform-specific address management."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this provider serves."""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def program_id(self) -> Pubkey:
|
||||
"""Get the main program ID for this platform."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_system_addresses(self) -> dict[str, Pubkey]:
|
||||
"""Get all system addresses required for this platform.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping address names to Pubkey objects
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def derive_pool_address(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey:
|
||||
"""Derive the pool/curve address for trading pair.
|
||||
|
||||
Args:
|
||||
base_mint: Base token mint address
|
||||
quote_mint: Quote token mint address (if applicable)
|
||||
|
||||
Returns:
|
||||
Pool/curve address for the trading pair
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
|
||||
"""Derive user's token account address.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
mint: Token mint address
|
||||
|
||||
Returns:
|
||||
User's token account address
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
||||
"""Get platform-specific additional accounts needed for trading.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
|
||||
Returns:
|
||||
Dictionary of additional account addresses
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InstructionBuilder(ABC):
|
||||
"""Abstract interface for building platform-specific trading instructions."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this builder serves."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def build_buy_instruction(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Instruction]:
|
||||
"""Build buy instruction(s) for the platform.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of quote tokens to spend
|
||||
minimum_amount_out: Minimum base tokens expected
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the buy operation
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def build_sell_instruction(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Instruction]:
|
||||
"""Build sell instruction(s) for the platform.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of base tokens to sell
|
||||
minimum_amount_out: Minimum quote tokens expected
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the sell operation
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_required_accounts_for_buy(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for buy operation (for priority fee calculation).
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_required_accounts_for_sell(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for sell operation (for priority fee calculation).
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class CurveManager(ABC):
|
||||
"""Abstract interface for platform-specific price calculations and pool state management."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this manager serves."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get the current state of a trading pool/curve.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool/curve
|
||||
|
||||
Returns:
|
||||
Dictionary containing pool state data
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def calculate_price(self, pool_address: Pubkey) -> float:
|
||||
"""Calculate current token price from pool state.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool/curve
|
||||
|
||||
Returns:
|
||||
Current token price in quote token units
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def calculate_buy_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected tokens received for a buy operation.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool/curve
|
||||
amount_in: Amount of quote tokens to spend
|
||||
|
||||
Returns:
|
||||
Expected amount of base tokens to receive
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def calculate_sell_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected quote tokens received for a sell operation.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool/curve
|
||||
amount_in: Amount of base tokens to sell
|
||||
|
||||
Returns:
|
||||
Expected amount of quote tokens to receive
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]:
|
||||
"""Get current pool reserves.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool/curve
|
||||
|
||||
Returns:
|
||||
Tuple of (base_reserves, quote_reserves)
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class EventParser(ABC):
|
||||
"""Abstract interface for parsing platform-specific token creation events."""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this parser serves."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def parse_token_creation_from_logs(
|
||||
self,
|
||||
logs: list[str],
|
||||
signature: str
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from transaction logs.
|
||||
|
||||
Args:
|
||||
logs: List of log strings from transaction
|
||||
signature: Transaction signature
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def parse_token_creation_from_instruction(
|
||||
self,
|
||||
instruction_data: bytes,
|
||||
accounts: list[int],
|
||||
account_keys: list[bytes]
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from instruction data.
|
||||
|
||||
Args:
|
||||
instruction_data: Raw instruction data
|
||||
accounts: List of account indices
|
||||
account_keys: List of account public keys
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def parse_token_creation_from_geyser(
|
||||
self,
|
||||
transaction_info: Any
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from Geyser transaction data.
|
||||
|
||||
Args:
|
||||
transaction_info: Geyser transaction information
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_program_id(self) -> Pubkey:
|
||||
"""Get the program ID this parser monitors.
|
||||
|
||||
Returns:
|
||||
Program ID for event filtering
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_instruction_discriminators(self) -> list[bytes]:
|
||||
"""Get instruction discriminators for token creation.
|
||||
|
||||
Returns:
|
||||
List of discriminator bytes to match
|
||||
"""
|
||||
pass
|
||||
@@ -0,0 +1,282 @@
|
||||
"""
|
||||
Platform factory and registry for managing multiple trading platforms.
|
||||
|
||||
This module provides a centralized way to instantiate and access
|
||||
platform-specific implementations of the trading interfaces.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Optional, Type
|
||||
|
||||
from core.client import SolanaClient
|
||||
from interfaces.core import (
|
||||
AddressProvider,
|
||||
CurveManager,
|
||||
EventParser,
|
||||
InstructionBuilder,
|
||||
Platform,
|
||||
)
|
||||
|
||||
from . import letsbonk, pumpfun
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlatformImplementations:
|
||||
"""Container for all platform-specific implementations."""
|
||||
address_provider: AddressProvider
|
||||
instruction_builder: InstructionBuilder
|
||||
curve_manager: CurveManager
|
||||
event_parser: EventParser
|
||||
|
||||
|
||||
class PlatformRegistry:
|
||||
"""Registry for platform implementations."""
|
||||
|
||||
def __init__(self):
|
||||
self._implementations: dict[Platform, dict[str, type]] = {}
|
||||
self._instances: dict[Platform, PlatformImplementations] = {}
|
||||
|
||||
def register_platform(
|
||||
self,
|
||||
platform: Platform,
|
||||
address_provider_class: type[AddressProvider],
|
||||
instruction_builder_class: type[InstructionBuilder],
|
||||
curve_manager_class: type[CurveManager],
|
||||
event_parser_class: type[EventParser]
|
||||
) -> None:
|
||||
"""Register platform implementations.
|
||||
|
||||
Args:
|
||||
platform: Platform enum value
|
||||
address_provider_class: AddressProvider implementation class
|
||||
instruction_builder_class: InstructionBuilder implementation class
|
||||
curve_manager_class: CurveManager implementation class
|
||||
event_parser_class: EventParser implementation class
|
||||
"""
|
||||
self._implementations[platform] = {
|
||||
'address_provider': address_provider_class,
|
||||
'instruction_builder': instruction_builder_class,
|
||||
'curve_manager': curve_manager_class,
|
||||
'event_parser': event_parser_class
|
||||
}
|
||||
|
||||
def create_platform_implementations(
|
||||
self,
|
||||
platform: Platform,
|
||||
client: SolanaClient,
|
||||
**kwargs: Any
|
||||
) -> PlatformImplementations:
|
||||
"""Create platform implementation instances.
|
||||
|
||||
Args:
|
||||
platform: Platform to create implementations for
|
||||
client: Solana RPC client
|
||||
**kwargs: Additional arguments for implementation constructors
|
||||
|
||||
Returns:
|
||||
PlatformImplementations containing all interface implementations
|
||||
|
||||
Raises:
|
||||
ValueError: If platform is not registered
|
||||
"""
|
||||
if platform not in self._implementations:
|
||||
raise ValueError(f"Platform {platform} is not registered")
|
||||
|
||||
# Check if we already have instances for this platform
|
||||
if platform in self._instances:
|
||||
return self._instances[platform]
|
||||
|
||||
impl_classes = self._implementations[platform]
|
||||
|
||||
# Create instances
|
||||
address_provider = impl_classes['address_provider']()
|
||||
instruction_builder = impl_classes['instruction_builder']()
|
||||
curve_manager = impl_classes['curve_manager'](client)
|
||||
event_parser = impl_classes['event_parser']()
|
||||
|
||||
implementations = PlatformImplementations(
|
||||
address_provider=address_provider,
|
||||
instruction_builder=instruction_builder,
|
||||
curve_manager=curve_manager,
|
||||
event_parser=event_parser
|
||||
)
|
||||
|
||||
# Cache the instances
|
||||
self._instances[platform] = implementations
|
||||
|
||||
return implementations
|
||||
|
||||
def get_platform_implementations(self, platform: Platform) -> PlatformImplementations | None:
|
||||
"""Get cached platform implementations.
|
||||
|
||||
Args:
|
||||
platform: Platform to get implementations for
|
||||
|
||||
Returns:
|
||||
PlatformImplementations if available, None otherwise
|
||||
"""
|
||||
return self._instances.get(platform)
|
||||
|
||||
def get_supported_platforms(self) -> list[Platform]:
|
||||
"""Get list of supported platforms.
|
||||
|
||||
Returns:
|
||||
List of registered platforms
|
||||
"""
|
||||
return list(self._implementations.keys())
|
||||
|
||||
def is_platform_supported(self, platform: Platform) -> bool:
|
||||
"""Check if a platform is supported.
|
||||
|
||||
Args:
|
||||
platform: Platform to check
|
||||
|
||||
Returns:
|
||||
True if platform is registered, False otherwise
|
||||
"""
|
||||
return platform in self._implementations
|
||||
|
||||
|
||||
class PlatformFactory:
|
||||
"""Factory for creating platform-specific implementations."""
|
||||
|
||||
def __init__(self):
|
||||
self.registry = PlatformRegistry()
|
||||
self._setup_default_platforms()
|
||||
|
||||
def _setup_default_platforms(self) -> None:
|
||||
"""Setup default platform registrations.
|
||||
|
||||
This will be populated as we implement each platform.
|
||||
"""
|
||||
# Platforms will be registered here as they are implemented
|
||||
# Example:
|
||||
# from platforms.pumpfun import PumpFunAddressProvider, PumpFunInstructionBuilder, ...
|
||||
# self.registry.register_platform(
|
||||
# Platform.PUMP_FUN,
|
||||
# PumpFunAddressProvider,
|
||||
# PumpFunInstructionBuilder,
|
||||
# PumpFunCurveManager,
|
||||
# PumpFunEventParser
|
||||
# )
|
||||
pass
|
||||
|
||||
def create_for_platform(
|
||||
self,
|
||||
platform: Platform,
|
||||
client: SolanaClient,
|
||||
**config: Any
|
||||
) -> PlatformImplementations:
|
||||
"""Create all implementations for a specific platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to create implementations for
|
||||
client: Solana RPC client
|
||||
**config: Platform-specific configuration
|
||||
|
||||
Returns:
|
||||
PlatformImplementations containing all interface implementations
|
||||
"""
|
||||
return self.registry.create_platform_implementations(platform, client, **config)
|
||||
|
||||
def get_address_provider(self, platform: Platform, client: SolanaClient) -> AddressProvider:
|
||||
"""Get address provider for platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to get provider for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
AddressProvider implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
return implementations.address_provider
|
||||
|
||||
def get_instruction_builder(self, platform: Platform, client: SolanaClient) -> InstructionBuilder:
|
||||
"""Get instruction builder for platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to get builder for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
InstructionBuilder implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
return implementations.instruction_builder
|
||||
|
||||
def get_curve_manager(self, platform: Platform, client: SolanaClient) -> CurveManager:
|
||||
"""Get curve manager for platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to get manager for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
CurveManager implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
return implementations.curve_manager
|
||||
|
||||
def get_event_parser(self, platform: Platform, client: SolanaClient) -> EventParser:
|
||||
"""Get event parser for platform.
|
||||
|
||||
Args:
|
||||
platform: Platform to get parser for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
EventParser implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
return implementations.event_parser
|
||||
|
||||
def get_supported_platforms(self) -> list[Platform]:
|
||||
"""Get list of supported platforms.
|
||||
|
||||
Returns:
|
||||
List of supported platforms
|
||||
"""
|
||||
return self.registry.get_supported_platforms()
|
||||
|
||||
|
||||
# Global factory instance
|
||||
platform_factory = PlatformFactory()
|
||||
|
||||
|
||||
def get_platform_implementations(platform: Platform, client: SolanaClient) -> PlatformImplementations:
|
||||
"""Convenience function to get platform implementations.
|
||||
|
||||
Args:
|
||||
platform: Platform to get implementations for
|
||||
client: Solana RPC client
|
||||
|
||||
Returns:
|
||||
PlatformImplementations containing all interface implementations
|
||||
"""
|
||||
return platform_factory.create_for_platform(platform, client)
|
||||
|
||||
|
||||
def register_platform_implementations(
|
||||
platform: Platform,
|
||||
address_provider_class: type[AddressProvider],
|
||||
instruction_builder_class: type[InstructionBuilder],
|
||||
curve_manager_class: type[CurveManager],
|
||||
event_parser_class: type[EventParser]
|
||||
) -> None:
|
||||
"""Register platform implementations with the global factory.
|
||||
|
||||
Args:
|
||||
platform: Platform enum value
|
||||
address_provider_class: AddressProvider implementation class
|
||||
instruction_builder_class: InstructionBuilder implementation class
|
||||
curve_manager_class: CurveManager implementation class
|
||||
event_parser_class: EventParser implementation class
|
||||
"""
|
||||
platform_factory.registry.register_platform(
|
||||
platform,
|
||||
address_provider_class,
|
||||
instruction_builder_class,
|
||||
curve_manager_class,
|
||||
event_parser_class
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
LetsBonk platform registration and exports.
|
||||
|
||||
This module registers all LetsBonk implementations with the platform factory
|
||||
and provides convenient imports for the LetsBonk platform.
|
||||
"""
|
||||
|
||||
from interfaces.core import Platform
|
||||
from platforms import register_platform_implementations
|
||||
|
||||
from .address_provider import LetsBonkAddressProvider
|
||||
from .curve_manager import LetsBonkCurveManager
|
||||
from .event_parser import LetsBonkEventParser
|
||||
from .instruction_builder import LetsBonkInstructionBuilder
|
||||
|
||||
# Register LetsBonk platform implementations
|
||||
register_platform_implementations(
|
||||
Platform.LETS_BONK,
|
||||
LetsBonkAddressProvider,
|
||||
LetsBonkInstructionBuilder,
|
||||
LetsBonkCurveManager,
|
||||
LetsBonkEventParser
|
||||
)
|
||||
|
||||
# Export implementations for direct use if needed
|
||||
__all__ = [
|
||||
'LetsBonkAddressProvider',
|
||||
'LetsBonkCurveManager',
|
||||
'LetsBonkEventParser',
|
||||
'LetsBonkInstructionBuilder'
|
||||
]
|
||||
@@ -0,0 +1,244 @@
|
||||
"""
|
||||
LetsBonk implementation of AddressProvider interface.
|
||||
|
||||
This module provides all LetsBonk (Raydium LaunchLab) specific addresses and PDA derivations
|
||||
by implementing the AddressProvider interface.
|
||||
"""
|
||||
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
from spl.token.instructions import get_associated_token_address
|
||||
|
||||
from interfaces.core import AddressProvider, Platform, TokenInfo
|
||||
|
||||
|
||||
class LetsBonkAddressProvider(AddressProvider):
|
||||
"""LetsBonk (Raydium LaunchLab) implementation of AddressProvider interface."""
|
||||
|
||||
# Raydium LaunchLab program addresses
|
||||
RAYDIUM_LAUNCHLAB_PROGRAM_ID = Pubkey.from_string("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj")
|
||||
GLOBAL_CONFIG = Pubkey.from_string("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX")
|
||||
LETSBONK_PLATFORM_CONFIG = Pubkey.from_string("FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1")
|
||||
|
||||
# System program addresses
|
||||
TOKEN_PROGRAM_ID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||
SYSTEM_PROGRAM_ID = Pubkey.from_string("11111111111111111111111111111111")
|
||||
WSOL_MINT = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
||||
ASSOCIATED_TOKEN_PROGRAM_ID = Pubkey.from_string("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
|
||||
SYSTEM_RENT_PROGRAM_ID = Pubkey.from_string("SysvarRent111111111111111111111111111111111")
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this provider serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
@property
|
||||
def program_id(self) -> Pubkey:
|
||||
"""Get the main program ID for this platform."""
|
||||
return self.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
|
||||
def get_system_addresses(self) -> dict[str, Pubkey]:
|
||||
"""Get all system addresses required for LetsBonk.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping address names to Pubkey objects
|
||||
"""
|
||||
return {
|
||||
# Raydium LaunchLab specific addresses
|
||||
"program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID,
|
||||
"global_config": self.GLOBAL_CONFIG,
|
||||
"platform_config": self.LETSBONK_PLATFORM_CONFIG,
|
||||
|
||||
# System addresses
|
||||
"system_program": self.SYSTEM_PROGRAM_ID,
|
||||
"token_program": self.TOKEN_PROGRAM_ID,
|
||||
"associated_token_program": self.ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
"rent": self.SYSTEM_RENT_PROGRAM_ID,
|
||||
"wsol_mint": self.WSOL_MINT,
|
||||
}
|
||||
|
||||
def derive_pool_address(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey:
|
||||
"""Derive the pool state address for a token pair.
|
||||
|
||||
For LetsBonk, this derives the pool state PDA using base_mint and WSOL.
|
||||
|
||||
Args:
|
||||
base_mint: Base token mint address
|
||||
quote_mint: Quote token mint (defaults to WSOL)
|
||||
|
||||
Returns:
|
||||
Pool state address
|
||||
"""
|
||||
if quote_mint is None:
|
||||
quote_mint = self.WSOL_MINT
|
||||
|
||||
pool_state, _ = Pubkey.find_program_address(
|
||||
[b"pool", bytes(base_mint), bytes(quote_mint)],
|
||||
self.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
)
|
||||
return pool_state
|
||||
|
||||
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
|
||||
"""Derive user's associated token account address.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
mint: Token mint address
|
||||
|
||||
Returns:
|
||||
User's associated token account address
|
||||
"""
|
||||
return get_associated_token_address(user, mint)
|
||||
|
||||
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
||||
"""Get LetsBonk-specific additional accounts needed for trading.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
|
||||
Returns:
|
||||
Dictionary of additional account addresses
|
||||
"""
|
||||
accounts = {}
|
||||
|
||||
# Add pool state if available
|
||||
if token_info.pool_state:
|
||||
accounts["pool_state"] = token_info.pool_state
|
||||
|
||||
# Add vault addresses if available
|
||||
if token_info.base_vault:
|
||||
accounts["base_vault"] = token_info.base_vault
|
||||
if token_info.quote_vault:
|
||||
accounts["quote_vault"] = token_info.quote_vault
|
||||
|
||||
# Derive pool state if not provided
|
||||
if not token_info.pool_state:
|
||||
accounts["pool_state"] = self.derive_pool_address(token_info.mint)
|
||||
|
||||
# Derive authority PDA
|
||||
accounts["authority"] = self.derive_authority_pda()
|
||||
|
||||
# Derive event authority PDA
|
||||
accounts["event_authority"] = self.derive_event_authority_pda()
|
||||
|
||||
return accounts
|
||||
|
||||
def derive_authority_pda(self) -> Pubkey:
|
||||
"""Derive the authority PDA for Raydium LaunchLab.
|
||||
|
||||
This PDA acts as the authority for pool vault operations.
|
||||
|
||||
Returns:
|
||||
Authority PDA address
|
||||
"""
|
||||
AUTH_SEED = b"vault_auth_seed"
|
||||
authority_pda, _ = Pubkey.find_program_address(
|
||||
[AUTH_SEED],
|
||||
self.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
)
|
||||
return authority_pda
|
||||
|
||||
def derive_event_authority_pda(self) -> Pubkey:
|
||||
"""Derive the event authority PDA for Raydium LaunchLab.
|
||||
|
||||
This PDA is used for emitting program events during swaps.
|
||||
|
||||
Returns:
|
||||
Event authority PDA address
|
||||
"""
|
||||
EVENT_AUTHORITY_SEED = b"__event_authority"
|
||||
event_authority_pda, _ = Pubkey.find_program_address(
|
||||
[EVENT_AUTHORITY_SEED],
|
||||
self.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
)
|
||||
return event_authority_pda
|
||||
|
||||
def create_wsol_account_with_seed(self, payer: Pubkey, seed: str) -> Pubkey:
|
||||
"""Create a WSOL account address using createAccountWithSeed pattern.
|
||||
|
||||
Args:
|
||||
payer: The account that will pay for and own the new account
|
||||
seed: String seed for deterministic account generation
|
||||
|
||||
Returns:
|
||||
New WSOL account address
|
||||
"""
|
||||
return Pubkey.create_with_seed(payer, seed, self.TOKEN_PROGRAM_ID)
|
||||
|
||||
def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a buy instruction.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for buy instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
return {
|
||||
"payer": user,
|
||||
"authority": additional_accounts["authority"],
|
||||
"global_config": self.GLOBAL_CONFIG,
|
||||
"platform_config": self.LETSBONK_PLATFORM_CONFIG,
|
||||
"pool_state": additional_accounts["pool_state"],
|
||||
"user_base_token": self.derive_user_token_account(user, token_info.mint),
|
||||
"base_vault": additional_accounts.get("base_vault", token_info.base_vault),
|
||||
"quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault),
|
||||
"base_token_mint": token_info.mint,
|
||||
"quote_token_mint": self.WSOL_MINT,
|
||||
"base_token_program": self.TOKEN_PROGRAM_ID,
|
||||
"quote_token_program": self.TOKEN_PROGRAM_ID,
|
||||
"event_authority": additional_accounts["event_authority"],
|
||||
"program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID,
|
||||
}
|
||||
|
||||
def get_sell_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a sell instruction.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for sell instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
return {
|
||||
"payer": user,
|
||||
"authority": additional_accounts["authority"],
|
||||
"global_config": self.GLOBAL_CONFIG,
|
||||
"platform_config": self.LETSBONK_PLATFORM_CONFIG,
|
||||
"pool_state": additional_accounts["pool_state"],
|
||||
"user_base_token": self.derive_user_token_account(user, token_info.mint),
|
||||
"base_vault": additional_accounts.get("base_vault", token_info.base_vault),
|
||||
"quote_vault": additional_accounts.get("quote_vault", token_info.quote_vault),
|
||||
"base_token_mint": token_info.mint,
|
||||
"quote_token_mint": self.WSOL_MINT,
|
||||
"base_token_program": self.TOKEN_PROGRAM_ID,
|
||||
"quote_token_program": self.TOKEN_PROGRAM_ID,
|
||||
"event_authority": additional_accounts["event_authority"],
|
||||
"program": self.RAYDIUM_LAUNCHLAB_PROGRAM_ID,
|
||||
}
|
||||
|
||||
def get_wsol_account_creation_accounts(self, user: Pubkey, wsol_account: Pubkey) -> dict[str, Pubkey]:
|
||||
"""Get accounts needed for WSOL account creation and initialization.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
wsol_account: WSOL account to be created
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for WSOL operations
|
||||
"""
|
||||
return {
|
||||
"payer": user,
|
||||
"wsol_account": wsol_account,
|
||||
"wsol_mint": self.WSOL_MINT,
|
||||
"owner": user,
|
||||
"system_program": self.SYSTEM_PROGRAM_ID,
|
||||
"token_program": self.TOKEN_PROGRAM_ID,
|
||||
"rent": self.SYSTEM_RENT_PROGRAM_ID,
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
LetsBonk implementation of CurveManager interface.
|
||||
|
||||
This module handles LetsBonk (Raydium LaunchLab) specific pool operations
|
||||
by implementing the CurveManager interface using IDL-based decoding.
|
||||
"""
|
||||
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from core.client import SolanaClient
|
||||
from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
|
||||
from interfaces.core import CurveManager, Platform
|
||||
from platforms.letsbonk.address_provider import LetsBonkAddressProvider
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Pool state discriminator for Raydium LaunchLab
|
||||
POOL_STATE_DISCRIMINATOR = bytes([247, 237, 227, 245, 215, 195, 222, 70])
|
||||
|
||||
|
||||
class LetsBonkCurveManager(CurveManager):
|
||||
"""LetsBonk (Raydium LaunchLab) implementation of CurveManager interface."""
|
||||
|
||||
def __init__(self, client: SolanaClient):
|
||||
"""Initialize LetsBonk curve manager.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
"""
|
||||
self.client = client
|
||||
self.address_provider = LetsBonkAddressProvider()
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this manager serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get the current state of a LetsBonk pool.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state account
|
||||
|
||||
Returns:
|
||||
Dictionary containing pool state data
|
||||
"""
|
||||
try:
|
||||
account = await self.client.get_account_info(pool_address)
|
||||
if not account.data:
|
||||
raise ValueError(f"No data in pool state account {pool_address}")
|
||||
|
||||
# Decode pool state (simplified - in production you'd use IDL parser)
|
||||
pool_state_data = self._decode_pool_state(account.data)
|
||||
|
||||
return pool_state_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get pool state: {e!s}")
|
||||
raise ValueError(f"Invalid pool state: {e!s}")
|
||||
|
||||
async def calculate_price(self, pool_address: Pubkey) -> float:
|
||||
"""Calculate current token price from pool state.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
|
||||
Returns:
|
||||
Current token price in SOL
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
# Use virtual reserves for price calculation
|
||||
virtual_base = pool_state["virtual_base"]
|
||||
virtual_quote = pool_state["virtual_quote"]
|
||||
|
||||
if virtual_base <= 0 or virtual_quote <= 0:
|
||||
raise ValueError("Invalid reserve state")
|
||||
|
||||
# Price = quote_reserves / base_reserves (how much SOL per token)
|
||||
price_lamports = virtual_quote / virtual_base
|
||||
price_sol = price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
|
||||
|
||||
return price_sol
|
||||
|
||||
async def calculate_buy_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected tokens received for a buy operation.
|
||||
|
||||
Uses the constant product AMM formula.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
amount_in: Amount of SOL to spend (in lamports)
|
||||
|
||||
Returns:
|
||||
Expected amount of tokens to receive (in raw token units)
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
virtual_base = pool_state["virtual_base"]
|
||||
virtual_quote = pool_state["virtual_quote"]
|
||||
|
||||
# Constant product formula: tokens_out = (amount_in * virtual_base) / (virtual_quote + amount_in)
|
||||
numerator = amount_in * virtual_base
|
||||
denominator = virtual_quote + amount_in
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
tokens_out = numerator // denominator
|
||||
return tokens_out
|
||||
|
||||
async def calculate_sell_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected SOL received for a sell operation.
|
||||
|
||||
Uses the constant product AMM formula.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
amount_in: Amount of tokens to sell (in raw token units)
|
||||
|
||||
Returns:
|
||||
Expected amount of SOL to receive (in lamports)
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
virtual_base = pool_state["virtual_base"]
|
||||
virtual_quote = pool_state["virtual_quote"]
|
||||
|
||||
# Constant product formula: sol_out = (amount_in * virtual_quote) / (virtual_base + amount_in)
|
||||
numerator = amount_in * virtual_quote
|
||||
denominator = virtual_base + amount_in
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
sol_out = numerator // denominator
|
||||
return sol_out
|
||||
|
||||
async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]:
|
||||
"""Get current pool reserves.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
|
||||
Returns:
|
||||
Tuple of (base_reserves, quote_reserves) in raw units
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
return (pool_state["virtual_base"], pool_state["virtual_quote"])
|
||||
|
||||
def _decode_pool_state(self, data: bytes) -> dict[str, Any]:
|
||||
"""Decode pool state data from raw bytes.
|
||||
|
||||
This is a simplified decoder. In production, you should use the IDL parser.
|
||||
|
||||
Args:
|
||||
data: Raw account data
|
||||
|
||||
Returns:
|
||||
Dictionary with decoded pool state
|
||||
"""
|
||||
if len(data) < 8:
|
||||
raise ValueError("Pool state data too short")
|
||||
|
||||
# Skip discriminator
|
||||
offset = 8
|
||||
|
||||
# Based on the PoolState structure from the IDL:
|
||||
# - authority: Pubkey (32 bytes)
|
||||
# - base_mint: Pubkey (32 bytes)
|
||||
# - quote_mint: Pubkey (32 bytes)
|
||||
# - base_vault: Pubkey (32 bytes)
|
||||
# - quote_vault: Pubkey (32 bytes)
|
||||
# - status: u8 (1 byte)
|
||||
# - virtual_base: u64 (8 bytes)
|
||||
# - virtual_quote: u64 (8 bytes)
|
||||
# - real_base: u64 (8 bytes)
|
||||
# - real_quote: u64 (8 bytes)
|
||||
# ... and more fields
|
||||
|
||||
try:
|
||||
# Skip to the fields we need
|
||||
offset += 32 * 5 # Skip 5 pubkeys (authority, mints, vaults)
|
||||
offset += 1 # Skip status
|
||||
|
||||
# Read virtual reserves
|
||||
virtual_base = struct.unpack_from("<Q", data, offset)[0]
|
||||
offset += 8
|
||||
|
||||
virtual_quote = struct.unpack_from("<Q", data, offset)[0]
|
||||
offset += 8
|
||||
|
||||
# Read real reserves
|
||||
real_base = struct.unpack_from("<Q", data, offset)[0]
|
||||
offset += 8
|
||||
|
||||
real_quote = struct.unpack_from("<Q", data, offset)[0]
|
||||
offset += 8
|
||||
|
||||
return {
|
||||
"virtual_base": virtual_base,
|
||||
"virtual_quote": virtual_quote,
|
||||
"real_base": real_base,
|
||||
"real_quote": real_quote,
|
||||
"price_per_token": (virtual_quote / virtual_base) * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL if virtual_base > 0 else 0,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to decode pool state: {e}")
|
||||
# Return some default values for testing
|
||||
return {
|
||||
"virtual_base": 1_000_000_000, # 1000 tokens with 6 decimals
|
||||
"virtual_quote": 1_000_000_000, # 1 SOL
|
||||
"real_base": 1_000_000_000,
|
||||
"real_quote": 1_000_000_000,
|
||||
"price_per_token": 0.001, # 0.001 SOL per token
|
||||
}
|
||||
|
||||
async def get_pool_info(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get detailed pool information including status and progress.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
|
||||
Returns:
|
||||
Dictionary with pool information
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
# Calculate additional metrics
|
||||
sol_raised = pool_state["real_quote"] / LAMPORTS_PER_SOL
|
||||
tokens_sold = (pool_state["virtual_base"] - pool_state["real_base"]) / 10**TOKEN_DECIMALS
|
||||
|
||||
return {
|
||||
"virtual_base_reserves": pool_state["virtual_base"],
|
||||
"virtual_quote_reserves": pool_state["virtual_quote"],
|
||||
"real_base_reserves": pool_state["real_base"],
|
||||
"real_quote_reserves": pool_state["real_quote"],
|
||||
"sol_raised": sol_raised,
|
||||
"tokens_sold": tokens_sold,
|
||||
"current_price": pool_state["price_per_token"],
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
"""
|
||||
LetsBonk implementation of EventParser interface.
|
||||
|
||||
This module parses LetsBonk-specific token creation events from various sources
|
||||
by implementing the EventParser interface.
|
||||
"""
|
||||
|
||||
import struct
|
||||
from time import monotonic
|
||||
from typing import Any, Final
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from interfaces.core import EventParser, Platform, TokenInfo
|
||||
from platforms.letsbonk.address_provider import LetsBonkAddressProvider
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class LetsBonkEventParser(EventParser):
|
||||
"""LetsBonk implementation of EventParser interface."""
|
||||
|
||||
# Discriminator for initialize instruction from IDL
|
||||
INITIALIZE_DISCRIMINATOR: Final[bytes] = bytes([175, 175, 109, 31, 13, 152, 155, 237])
|
||||
INITIALIZE_DISCRIMINATOR_INT: Final[int] = struct.unpack("<Q", INITIALIZE_DISCRIMINATOR)[0]
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize LetsBonk event parser."""
|
||||
self.address_provider = LetsBonkAddressProvider()
|
||||
|
||||
@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.
|
||||
|
||||
Args:
|
||||
instruction_data: Raw instruction data
|
||||
accounts: List of account indices
|
||||
account_keys: List of account public keys
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
if not instruction_data.startswith(self.INITIALIZE_DISCRIMINATOR):
|
||||
return None
|
||||
|
||||
try:
|
||||
# Helper to get account key
|
||||
def get_account_key(index):
|
||||
if index >= len(accounts):
|
||||
return None
|
||||
account_index = accounts[index]
|
||||
if account_index >= len(account_keys):
|
||||
return None
|
||||
return Pubkey.from_bytes(account_keys[account_index])
|
||||
|
||||
# Parse instruction data
|
||||
token_data = self._parse_initialize_instruction_data(instruction_data)
|
||||
if not token_data:
|
||||
return None
|
||||
|
||||
# Extract account information based on IDL account order
|
||||
creator = get_account_key(1) # creator account
|
||||
pool_state = get_account_key(5) # pool_state account
|
||||
base_mint = get_account_key(6) # base_mint account
|
||||
base_vault = get_account_key(8) # base_vault account
|
||||
quote_vault = get_account_key(9) # quote_vault account
|
||||
|
||||
if not all([creator, pool_state, base_mint, base_vault, quote_vault]):
|
||||
return None
|
||||
|
||||
return TokenInfo(
|
||||
name=token_data["name"],
|
||||
symbol=token_data["symbol"],
|
||||
uri=token_data["uri"],
|
||||
mint=base_mint,
|
||||
platform=Platform.LETS_BONK,
|
||||
pool_state=pool_state,
|
||||
base_vault=base_vault,
|
||||
quote_vault=quote_vault,
|
||||
user=creator,
|
||||
creator=creator,
|
||||
creation_timestamp=monotonic(),
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def parse_token_creation_from_geyser(
|
||||
self,
|
||||
transaction_info: Any
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from Geyser transaction data.
|
||||
|
||||
Args:
|
||||
transaction_info: Geyser transaction information
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
try:
|
||||
if not hasattr(transaction_info, 'transaction'):
|
||||
return None
|
||||
|
||||
tx = transaction_info.transaction.transaction.transaction
|
||||
msg = getattr(tx, "message", None)
|
||||
if msg is None:
|
||||
return None
|
||||
|
||||
for ix in msg.instructions:
|
||||
# Skip non-LetsBonk program instructions
|
||||
program_idx = ix.program_id_index
|
||||
if program_idx >= len(msg.account_keys):
|
||||
continue
|
||||
|
||||
program_id = msg.account_keys[program_idx]
|
||||
if bytes(program_id) != bytes(self.get_program_id()):
|
||||
continue
|
||||
|
||||
# Check if it's the LetsBonk platform config account
|
||||
has_platform_config = False
|
||||
for acc_idx in ix.accounts:
|
||||
if acc_idx < len(msg.account_keys):
|
||||
acc_key = msg.account_keys[acc_idx]
|
||||
if bytes(acc_key) == bytes(self.address_provider.LETSBONK_PLATFORM_CONFIG):
|
||||
has_platform_config = True
|
||||
break
|
||||
|
||||
if not has_platform_config:
|
||||
continue
|
||||
|
||||
# Process instruction data
|
||||
token_info = self.parse_token_creation_from_instruction(
|
||||
ix.data, ix.accounts, msg.account_keys
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_program_id(self) -> Pubkey:
|
||||
"""Get the Raydium LaunchLab program ID this parser monitors.
|
||||
|
||||
Returns:
|
||||
Raydium LaunchLab program ID
|
||||
"""
|
||||
return self.address_provider.RAYDIUM_LAUNCHLAB_PROGRAM_ID
|
||||
|
||||
def get_instruction_discriminators(self) -> list[bytes]:
|
||||
"""Get instruction discriminators for token creation.
|
||||
|
||||
Returns:
|
||||
List of discriminator bytes to match
|
||||
"""
|
||||
return [self.INITIALIZE_DISCRIMINATOR]
|
||||
|
||||
def _parse_initialize_instruction_data(self, data: bytes) -> dict | None:
|
||||
"""Parse the initialize instruction data from LetsBonk.
|
||||
|
||||
Args:
|
||||
data: Raw instruction data
|
||||
|
||||
Returns:
|
||||
Dictionary of parsed data or None if parsing fails
|
||||
"""
|
||||
if len(data) < 8:
|
||||
return None
|
||||
|
||||
# Check discriminator
|
||||
discriminator = struct.unpack("<Q", data[:8])[0]
|
||||
if discriminator != self.INITIALIZE_DISCRIMINATOR_INT:
|
||||
return None
|
||||
|
||||
offset = 8
|
||||
parsed_data = {}
|
||||
|
||||
try:
|
||||
# Helper functions for reading data
|
||||
def read_string():
|
||||
nonlocal offset
|
||||
if offset + 4 > len(data):
|
||||
raise ValueError("Not enough data for string length")
|
||||
length = struct.unpack_from("<I", data, offset)[0]
|
||||
offset += 4
|
||||
if offset + length > len(data):
|
||||
raise ValueError("Not enough data for string")
|
||||
value = data[offset:offset + length].decode('utf-8')
|
||||
offset += length
|
||||
return value
|
||||
|
||||
def read_u8():
|
||||
nonlocal offset
|
||||
if offset + 1 > len(data):
|
||||
raise ValueError("Not enough data for u8")
|
||||
value = struct.unpack_from("<B", data, offset)[0]
|
||||
offset += 1
|
||||
return value
|
||||
|
||||
# Parse MintParams struct
|
||||
decimals = read_u8()
|
||||
name = read_string()
|
||||
symbol = read_string()
|
||||
uri = read_string()
|
||||
|
||||
parsed_data["name"] = name
|
||||
parsed_data["symbol"] = symbol
|
||||
parsed_data["uri"] = uri
|
||||
parsed_data["decimals"] = decimals
|
||||
|
||||
return parsed_data
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def parse_token_creation_from_block(self, block_data: dict) -> list[TokenInfo]:
|
||||
"""Parse token creations from block data (for block listener).
|
||||
|
||||
Args:
|
||||
block_data: Block data from WebSocket
|
||||
|
||||
Returns:
|
||||
List of TokenInfo for any token creations found
|
||||
"""
|
||||
tokens = []
|
||||
|
||||
try:
|
||||
if "transactions" not in block_data:
|
||||
return tokens
|
||||
|
||||
for tx in block_data["transactions"]:
|
||||
if not isinstance(tx, dict) or "transaction" not in tx:
|
||||
continue
|
||||
|
||||
# Process transaction (implementation would be similar to pump.fun)
|
||||
# This is a simplified version - full implementation would decode
|
||||
# the transaction and check for LetsBonk initialize instructions
|
||||
pass
|
||||
|
||||
return tokens
|
||||
|
||||
except Exception:
|
||||
return tokens
|
||||
@@ -0,0 +1,402 @@
|
||||
"""
|
||||
LetsBonk implementation of InstructionBuilder interface.
|
||||
|
||||
This module builds LetsBonk (Raydium LaunchLab) specific buy and sell instructions
|
||||
by implementing the InstructionBuilder interface.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import struct
|
||||
import time
|
||||
from typing import Final
|
||||
|
||||
from solders.instruction import AccountMeta, Instruction
|
||||
from solders.pubkey import Pubkey
|
||||
from solders.system_program import CreateAccountWithSeedParams, create_account_with_seed
|
||||
from spl.token.instructions import create_idempotent_associated_token_account
|
||||
|
||||
from core.pubkeys import TOKEN_DECIMALS
|
||||
from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
|
||||
|
||||
# Instruction discriminators for LetsBonk (from IDL)
|
||||
BUY_EXACT_IN_DISCRIMINATOR: Final[bytes] = bytes([250, 234, 13, 123, 213, 156, 19, 236])
|
||||
BUY_EXACT_OUT_DISCRIMINATOR: Final[bytes] = bytes([24, 211, 116, 40, 105, 3, 153, 56])
|
||||
SELL_EXACT_IN_DISCRIMINATOR: Final[bytes] = bytes([149, 39, 222, 155, 211, 124, 152, 26])
|
||||
SELL_EXACT_OUT_DISCRIMINATOR: Final[bytes] = bytes([95, 200, 71, 34, 8, 9, 11, 166])
|
||||
|
||||
|
||||
class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
"""LetsBonk (Raydium LaunchLab) implementation of InstructionBuilder interface."""
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this builder serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
async def build_buy_instruction(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Instruction]:
|
||||
"""Build buy instruction(s) for LetsBonk using buy_exact_in.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of SOL to spend (in lamports)
|
||||
minimum_amount_out: Minimum tokens expected (raw token units)
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the buy operation
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
# Get all required accounts
|
||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||
|
||||
# 1. Create idempotent ATA for base token
|
||||
ata_instruction = create_idempotent_associated_token_account(
|
||||
user, # payer
|
||||
user, # owner
|
||||
token_info.mint, # mint
|
||||
address_provider.TOKEN_PROGRAM_ID, # token program
|
||||
)
|
||||
instructions.append(ata_instruction)
|
||||
|
||||
# 2. Create WSOL account with seed (temporary account for the transaction)
|
||||
wsol_seed = self._generate_wsol_seed(user)
|
||||
wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed)
|
||||
|
||||
# Account creation cost + amount to spend
|
||||
account_creation_lamports = 2_039_280 # Standard account creation cost
|
||||
total_lamports = amount_in + account_creation_lamports
|
||||
|
||||
create_wsol_ix = create_account_with_seed(
|
||||
CreateAccountWithSeedParams(
|
||||
from_pubkey=user,
|
||||
to_pubkey=wsol_account,
|
||||
base=user,
|
||||
seed=wsol_seed,
|
||||
lamports=total_lamports,
|
||||
space=165, # Size of a token account
|
||||
owner=address_provider.TOKEN_PROGRAM_ID
|
||||
)
|
||||
)
|
||||
instructions.append(create_wsol_ix)
|
||||
|
||||
# 3. Initialize WSOL account
|
||||
initialize_wsol_ix = self._create_initialize_account_instruction(
|
||||
wsol_account,
|
||||
address_provider.WSOL_MINT,
|
||||
user,
|
||||
address_provider
|
||||
)
|
||||
instructions.append(initialize_wsol_ix)
|
||||
|
||||
# 4. Build buy_exact_in instruction
|
||||
buy_accounts = [
|
||||
AccountMeta(pubkey=accounts_info["payer"], is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token
|
||||
AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
|
||||
SHARE_FEE_RATE = 0 # No sharing fee
|
||||
instruction_data = (
|
||||
BUY_EXACT_IN_DISCRIMINATOR +
|
||||
struct.pack("<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,
|
||||
address_provider
|
||||
)
|
||||
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 = 2_039_280
|
||||
|
||||
create_wsol_ix = create_account_with_seed(
|
||||
CreateAccountWithSeedParams(
|
||||
from_pubkey=user,
|
||||
to_pubkey=wsol_account,
|
||||
base=user,
|
||||
seed=wsol_seed,
|
||||
lamports=account_creation_lamports,
|
||||
space=165, # Size of a token account
|
||||
owner=address_provider.TOKEN_PROGRAM_ID
|
||||
)
|
||||
)
|
||||
instructions.append(create_wsol_ix)
|
||||
|
||||
# 2. Initialize WSOL account
|
||||
initialize_wsol_ix = self._create_initialize_account_instruction(
|
||||
wsol_account,
|
||||
address_provider.WSOL_MINT,
|
||||
user,
|
||||
address_provider
|
||||
)
|
||||
instructions.append(initialize_wsol_ix)
|
||||
|
||||
# 3. Build sell_exact_in instruction
|
||||
sell_accounts = [
|
||||
AccountMeta(pubkey=accounts_info["payer"], is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token
|
||||
AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["base_token_mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["quote_token_mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["base_token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["quote_token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
|
||||
SHARE_FEE_RATE = 0 # No sharing fee
|
||||
instruction_data = (
|
||||
SELL_EXACT_IN_DISCRIMINATOR +
|
||||
struct.pack("<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,
|
||||
address_provider
|
||||
)
|
||||
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"],
|
||||
accounts_info["base_token_mint"],
|
||||
accounts_info["quote_token_mint"],
|
||||
accounts_info["program"],
|
||||
]
|
||||
|
||||
def get_required_accounts_for_sell(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for sell operation (for priority fee calculation).
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
||||
|
||||
return [
|
||||
accounts_info["pool_state"],
|
||||
accounts_info["user_base_token"],
|
||||
accounts_info["base_vault"],
|
||||
accounts_info["quote_vault"],
|
||||
accounts_info["base_token_mint"],
|
||||
accounts_info["quote_token_mint"],
|
||||
accounts_info["program"],
|
||||
]
|
||||
|
||||
def _generate_wsol_seed(self, user: Pubkey) -> str:
|
||||
"""Generate a unique seed for WSOL account creation.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
|
||||
Returns:
|
||||
Unique seed string for WSOL account
|
||||
"""
|
||||
# Generate a unique seed based on timestamp and user pubkey
|
||||
seed_data = f"{int(time.time())}{user!s}"
|
||||
return hashlib.sha256(seed_data.encode()).hexdigest()[:32]
|
||||
|
||||
def _create_initialize_account_instruction(
|
||||
self,
|
||||
account: Pubkey,
|
||||
mint: Pubkey,
|
||||
owner: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> Instruction:
|
||||
"""Create an InitializeAccount instruction for the Token Program.
|
||||
|
||||
Args:
|
||||
account: The account to initialize
|
||||
mint: The token mint
|
||||
owner: The account owner
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
Instruction for initializing the account
|
||||
"""
|
||||
accounts = [
|
||||
AccountMeta(pubkey=account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=owner, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=address_provider.SYSTEM_RENT_PROGRAM_ID, is_signer=False, is_writable=False),
|
||||
]
|
||||
|
||||
# InitializeAccount instruction discriminator (instruction 1 in Token Program)
|
||||
data = bytes([1])
|
||||
|
||||
return Instruction(
|
||||
program_id=address_provider.TOKEN_PROGRAM_ID,
|
||||
data=data,
|
||||
accounts=accounts
|
||||
)
|
||||
|
||||
def _create_close_account_instruction(
|
||||
self,
|
||||
account: Pubkey,
|
||||
destination: Pubkey,
|
||||
owner: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
) -> Instruction:
|
||||
"""Create a CloseAccount instruction for the Token Program.
|
||||
|
||||
Args:
|
||||
account: The account to close
|
||||
destination: Where to send the remaining lamports
|
||||
owner: The account owner (must sign)
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
Instruction for closing the account
|
||||
"""
|
||||
accounts = [
|
||||
AccountMeta(pubkey=account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=destination, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=owner, is_signer=True, is_writable=False),
|
||||
]
|
||||
|
||||
# CloseAccount instruction discriminator (instruction 9 in Token Program)
|
||||
data = bytes([9])
|
||||
|
||||
return Instruction(
|
||||
program_id=address_provider.TOKEN_PROGRAM_ID,
|
||||
data=data,
|
||||
accounts=accounts
|
||||
)
|
||||
|
||||
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
|
||||
"""Convert decimal token amount to raw token units.
|
||||
|
||||
Args:
|
||||
token_amount_decimal: Token amount in decimal form
|
||||
|
||||
Returns:
|
||||
Token amount in raw units (adjusted for decimals)
|
||||
"""
|
||||
return int(token_amount_decimal * 10**TOKEN_DECIMALS)
|
||||
|
||||
def calculate_token_amount_decimal(self, token_amount_raw: int) -> float:
|
||||
"""Convert raw token amount to decimal form.
|
||||
|
||||
Args:
|
||||
token_amount_raw: Token amount in raw units
|
||||
|
||||
Returns:
|
||||
Token amount in decimal form
|
||||
"""
|
||||
return token_amount_raw / 10**TOKEN_DECIMALS
|
||||
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Pump.Fun platform registration and exports.
|
||||
|
||||
This module registers all pump.fun implementations with the platform factory
|
||||
and provides convenient imports for the pump.fun platform.
|
||||
"""
|
||||
|
||||
from interfaces.core import Platform
|
||||
from platforms import register_platform_implementations
|
||||
|
||||
from .address_provider import PumpFunAddressProvider
|
||||
from .curve_manager import PumpFunCurveManager
|
||||
from .event_parser import PumpFunEventParser
|
||||
from .instruction_builder import PumpFunInstructionBuilder
|
||||
|
||||
# Register pump.fun platform implementations
|
||||
register_platform_implementations(
|
||||
Platform.PUMP_FUN,
|
||||
PumpFunAddressProvider,
|
||||
PumpFunInstructionBuilder,
|
||||
PumpFunCurveManager,
|
||||
PumpFunEventParser
|
||||
)
|
||||
|
||||
# Export implementations for direct use if needed
|
||||
__all__ = [
|
||||
'PumpFunAddressProvider',
|
||||
'PumpFunCurveManager',
|
||||
'PumpFunEventParser',
|
||||
'PumpFunInstructionBuilder'
|
||||
]
|
||||
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
Pump.Fun implementation of AddressProvider interface.
|
||||
|
||||
This module provides all pump.fun-specific addresses and PDA derivations
|
||||
by implementing the AddressProvider interface.
|
||||
"""
|
||||
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
from spl.token.instructions import get_associated_token_address
|
||||
|
||||
from core.pubkeys import PumpAddresses, SystemAddresses
|
||||
from interfaces.core import AddressProvider, Platform, TokenInfo
|
||||
|
||||
|
||||
class PumpFunAddressProvider(AddressProvider):
|
||||
"""Pump.Fun implementation of AddressProvider interface."""
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this provider serves."""
|
||||
return Platform.PUMP_FUN
|
||||
|
||||
@property
|
||||
def program_id(self) -> Pubkey:
|
||||
"""Get the main program ID for this platform."""
|
||||
return PumpAddresses.PROGRAM
|
||||
|
||||
def get_system_addresses(self) -> dict[str, Pubkey]:
|
||||
"""Get all system addresses required for pump.fun.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping address names to Pubkey objects
|
||||
"""
|
||||
return {
|
||||
# Pump.fun specific addresses
|
||||
"program": PumpAddresses.PROGRAM,
|
||||
"global": PumpAddresses.GLOBAL,
|
||||
"event_authority": PumpAddresses.EVENT_AUTHORITY,
|
||||
"fee": PumpAddresses.FEE,
|
||||
"liquidity_migrator": PumpAddresses.LIQUIDITY_MIGRATOR,
|
||||
|
||||
# System addresses
|
||||
"system_program": SystemAddresses.PROGRAM,
|
||||
"token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"associated_token_program": SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
||||
"rent": SystemAddresses.RENT,
|
||||
"sol_mint": SystemAddresses.SOL,
|
||||
}
|
||||
|
||||
def derive_pool_address(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey:
|
||||
"""Derive the bonding curve address for a token.
|
||||
|
||||
For pump.fun, this is the bonding curve PDA derived from the mint.
|
||||
|
||||
Args:
|
||||
base_mint: Token mint address
|
||||
quote_mint: Not used for pump.fun (SOL is always the quote)
|
||||
|
||||
Returns:
|
||||
Bonding curve address
|
||||
"""
|
||||
bonding_curve, _ = Pubkey.find_program_address(
|
||||
[b"bonding-curve", bytes(base_mint)],
|
||||
PumpAddresses.PROGRAM
|
||||
)
|
||||
return bonding_curve
|
||||
|
||||
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
|
||||
"""Derive user's associated token account address.
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
mint: Token mint address
|
||||
|
||||
Returns:
|
||||
User's associated token account address
|
||||
"""
|
||||
return get_associated_token_address(user, mint)
|
||||
|
||||
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
||||
"""Get pump.fun-specific additional accounts needed for trading.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
|
||||
Returns:
|
||||
Dictionary of additional account addresses
|
||||
"""
|
||||
accounts = {}
|
||||
|
||||
# Add bonding curve if available
|
||||
if token_info.bonding_curve:
|
||||
accounts["bonding_curve"] = token_info.bonding_curve
|
||||
|
||||
# Add associated bonding curve if available
|
||||
if token_info.associated_bonding_curve:
|
||||
accounts["associated_bonding_curve"] = token_info.associated_bonding_curve
|
||||
|
||||
# Add creator vault if available
|
||||
if token_info.creator_vault:
|
||||
accounts["creator_vault"] = token_info.creator_vault
|
||||
|
||||
# Derive associated bonding curve if not provided
|
||||
if not token_info.associated_bonding_curve and token_info.bonding_curve:
|
||||
accounts["associated_bonding_curve"] = self.derive_associated_bonding_curve(
|
||||
token_info.mint, token_info.bonding_curve
|
||||
)
|
||||
|
||||
# Derive creator vault if not provided but creator is available
|
||||
if not token_info.creator_vault and token_info.creator:
|
||||
accounts["creator_vault"] = self.derive_creator_vault(token_info.creator)
|
||||
|
||||
return accounts
|
||||
|
||||
def derive_associated_bonding_curve(self, mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
|
||||
"""Derive the associated bonding curve (ATA of bonding curve for the token).
|
||||
|
||||
Args:
|
||||
mint: Token mint address
|
||||
bonding_curve: Bonding curve address
|
||||
|
||||
Returns:
|
||||
Associated bonding curve address
|
||||
"""
|
||||
return get_associated_token_address(bonding_curve, mint)
|
||||
|
||||
def derive_creator_vault(self, creator: Pubkey) -> Pubkey:
|
||||
"""Derive the creator vault address.
|
||||
|
||||
Args:
|
||||
creator: Creator address
|
||||
|
||||
Returns:
|
||||
Creator vault address
|
||||
"""
|
||||
creator_vault, _ = Pubkey.find_program_address(
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpAddresses.PROGRAM
|
||||
)
|
||||
return creator_vault
|
||||
|
||||
def derive_global_volume_accumulator(self) -> Pubkey:
|
||||
"""Derive the global volume accumulator PDA.
|
||||
|
||||
Returns:
|
||||
Global volume accumulator address
|
||||
"""
|
||||
return PumpAddresses.find_global_volume_accumulator()
|
||||
|
||||
def derive_user_volume_accumulator(self, user: Pubkey) -> Pubkey:
|
||||
"""Derive the user volume accumulator PDA.
|
||||
|
||||
Args:
|
||||
user: User address
|
||||
|
||||
Returns:
|
||||
User volume accumulator address
|
||||
"""
|
||||
return PumpAddresses.find_user_volume_accumulator(user)
|
||||
|
||||
def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a buy instruction.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for buy instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
return {
|
||||
"global": PumpAddresses.GLOBAL,
|
||||
"fee": PumpAddresses.FEE,
|
||||
"mint": token_info.mint,
|
||||
"bonding_curve": additional_accounts.get("bonding_curve", token_info.bonding_curve),
|
||||
"associated_bonding_curve": additional_accounts.get("associated_bonding_curve", token_info.associated_bonding_curve),
|
||||
"user_token_account": self.derive_user_token_account(user, token_info.mint),
|
||||
"user": user,
|
||||
"system_program": SystemAddresses.PROGRAM,
|
||||
"token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault),
|
||||
"event_authority": PumpAddresses.EVENT_AUTHORITY,
|
||||
"program": PumpAddresses.PROGRAM,
|
||||
"global_volume_accumulator": self.derive_global_volume_accumulator(),
|
||||
"user_volume_accumulator": self.derive_user_volume_accumulator(user),
|
||||
}
|
||||
|
||||
def get_sell_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a sell instruction.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for sell instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
return {
|
||||
"global": PumpAddresses.GLOBAL,
|
||||
"fee": PumpAddresses.FEE,
|
||||
"mint": token_info.mint,
|
||||
"bonding_curve": additional_accounts.get("bonding_curve", token_info.bonding_curve),
|
||||
"associated_bonding_curve": additional_accounts.get("associated_bonding_curve", token_info.associated_bonding_curve),
|
||||
"user_token_account": self.derive_user_token_account(user, token_info.mint),
|
||||
"user": user,
|
||||
"system_program": SystemAddresses.PROGRAM,
|
||||
"creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault),
|
||||
"token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"event_authority": PumpAddresses.EVENT_AUTHORITY,
|
||||
"program": PumpAddresses.PROGRAM,
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
"""
|
||||
Pump.Fun implementation of CurveManager interface.
|
||||
|
||||
This module handles pump.fun-specific bonding curve operations
|
||||
by implementing the CurveManager interface.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from core.client import SolanaClient
|
||||
from core.curve import BondingCurveManager
|
||||
from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
|
||||
from interfaces.core import CurveManager, Platform
|
||||
|
||||
|
||||
class PumpFunCurveManager(CurveManager):
|
||||
"""Pump.Fun implementation of CurveManager interface."""
|
||||
|
||||
def __init__(self, client: SolanaClient):
|
||||
"""Initialize pump.fun curve manager.
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
"""
|
||||
self.client = client
|
||||
self.bonding_curve_manager = BondingCurveManager(client)
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this manager serves."""
|
||||
return Platform.PUMP_FUN
|
||||
|
||||
async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get the current state of a pump.fun bonding curve.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
Returns:
|
||||
Dictionary containing bonding curve state data
|
||||
"""
|
||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
||||
|
||||
return {
|
||||
"virtual_token_reserves": curve_state.virtual_token_reserves,
|
||||
"virtual_sol_reserves": curve_state.virtual_sol_reserves,
|
||||
"real_token_reserves": curve_state.real_token_reserves,
|
||||
"real_sol_reserves": curve_state.real_sol_reserves,
|
||||
"token_total_supply": curve_state.token_total_supply,
|
||||
"complete": curve_state.complete,
|
||||
"creator": str(curve_state.creator),
|
||||
|
||||
# Calculated fields for convenience
|
||||
"price_per_token": curve_state.calculate_price(),
|
||||
"token_reserves_decimal": curve_state.token_reserves,
|
||||
"sol_reserves_decimal": curve_state.sol_reserves,
|
||||
}
|
||||
|
||||
async def calculate_price(self, pool_address: Pubkey) -> float:
|
||||
"""Calculate current token price from bonding curve state.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
Returns:
|
||||
Current token price in SOL
|
||||
"""
|
||||
return await self.bonding_curve_manager.calculate_price(pool_address)
|
||||
|
||||
async def calculate_buy_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected tokens received for a buy operation.
|
||||
|
||||
Uses the pump.fun bonding curve formula to calculate token output.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
amount_in: Amount of SOL to spend (in lamports)
|
||||
|
||||
Returns:
|
||||
Expected amount of tokens to receive (in raw token units)
|
||||
"""
|
||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
||||
|
||||
# Use virtual reserves for bonding curve calculation
|
||||
# Formula: tokens_out = (amount_in * virtual_token_reserves) / (virtual_sol_reserves + amount_in)
|
||||
numerator = amount_in * curve_state.virtual_token_reserves
|
||||
denominator = curve_state.virtual_sol_reserves + amount_in
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
tokens_out = numerator // denominator
|
||||
return tokens_out
|
||||
|
||||
async def calculate_sell_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected SOL received for a sell operation.
|
||||
|
||||
Uses the pump.fun bonding curve formula to calculate SOL output.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
amount_in: Amount of tokens to sell (in raw token units)
|
||||
|
||||
Returns:
|
||||
Expected amount of SOL to receive (in lamports)
|
||||
"""
|
||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
||||
|
||||
# Use virtual reserves for bonding curve calculation
|
||||
# Formula: sol_out = (amount_in * virtual_sol_reserves) / (virtual_token_reserves + amount_in)
|
||||
numerator = amount_in * curve_state.virtual_sol_reserves
|
||||
denominator = curve_state.virtual_token_reserves + amount_in
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
sol_out = numerator // denominator
|
||||
return sol_out
|
||||
|
||||
async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]:
|
||||
"""Get current bonding curve reserves.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
Returns:
|
||||
Tuple of (token_reserves, sol_reserves) in raw units
|
||||
"""
|
||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
||||
return (curve_state.virtual_token_reserves, curve_state.virtual_sol_reserves)
|
||||
|
||||
async def calculate_expected_tokens(self, pool_address: Pubkey, sol_amount: float) -> float:
|
||||
"""Calculate the expected token amount for a given SOL input.
|
||||
|
||||
This is a convenience method that wraps calculate_buy_amount_out
|
||||
and converts between decimal and raw units.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
sol_amount: Amount of SOL to spend (in decimal SOL)
|
||||
|
||||
Returns:
|
||||
Expected token amount (in decimal tokens)
|
||||
"""
|
||||
sol_lamports = int(sol_amount * LAMPORTS_PER_SOL)
|
||||
tokens_raw = await self.calculate_buy_amount_out(pool_address, sol_lamports)
|
||||
return tokens_raw / 10**TOKEN_DECIMALS
|
||||
|
||||
async def calculate_expected_sol(self, pool_address: Pubkey, token_amount: float) -> float:
|
||||
"""Calculate the expected SOL amount for a given token input.
|
||||
|
||||
This is a convenience method that wraps calculate_sell_amount_out
|
||||
and converts between decimal and raw units.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
token_amount: Amount of tokens to sell (in decimal tokens)
|
||||
|
||||
Returns:
|
||||
Expected SOL amount (in decimal SOL)
|
||||
"""
|
||||
tokens_raw = int(token_amount * 10**TOKEN_DECIMALS)
|
||||
sol_lamports = await self.calculate_sell_amount_out(pool_address, tokens_raw)
|
||||
return sol_lamports / LAMPORTS_PER_SOL
|
||||
|
||||
async def is_curve_complete(self, pool_address: Pubkey) -> bool:
|
||||
"""Check if the bonding curve is complete (migrated to Raydium).
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
Returns:
|
||||
True if curve is complete, False otherwise
|
||||
"""
|
||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
||||
return curve_state.complete
|
||||
|
||||
async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get bonding curve completion progress information.
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
Returns:
|
||||
Dictionary with progress information
|
||||
"""
|
||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
||||
|
||||
# Calculate progress based on SOL raised vs target
|
||||
# This is approximate since the exact target isn't stored in the curve state
|
||||
sol_raised = curve_state.real_sol_reserves / LAMPORTS_PER_SOL
|
||||
|
||||
# Estimate progress based on typical pump.fun graduation requirements
|
||||
# (This could be made more accurate with additional on-chain data)
|
||||
estimated_target_sol = 85.0 # Typical pump.fun graduation target
|
||||
progress_percentage = min((sol_raised / estimated_target_sol) * 100, 100.0)
|
||||
|
||||
return {
|
||||
"complete": curve_state.complete,
|
||||
"sol_raised": sol_raised,
|
||||
"estimated_target_sol": estimated_target_sol,
|
||||
"progress_percentage": progress_percentage,
|
||||
"tokens_available": curve_state.virtual_token_reserves / 10**TOKEN_DECIMALS,
|
||||
"market_cap_sol": sol_raised, # Approximate market cap
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
"""
|
||||
Pump.Fun implementation of EventParser interface.
|
||||
|
||||
This module parses pump.fun-specific token creation events from various sources
|
||||
by implementing the EventParser interface.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import struct
|
||||
from time import monotonic
|
||||
from typing import Any, Final
|
||||
|
||||
import base58
|
||||
from solders.pubkey import Pubkey
|
||||
from solders.transaction import VersionedTransaction
|
||||
|
||||
from core.pubkeys import PumpAddresses, SystemAddresses
|
||||
from interfaces.core import EventParser, Platform, TokenInfo
|
||||
|
||||
|
||||
class PumpFunEventParser(EventParser):
|
||||
"""Pump.Fun implementation of EventParser interface."""
|
||||
|
||||
# Discriminators for pump.fun instructions
|
||||
CREATE_DISCRIMINATOR: Final[int] = 8576854823835016728
|
||||
CREATE_DISCRIMINATOR_BYTES: Final[bytes] = struct.pack("<Q", CREATE_DISCRIMINATOR)
|
||||
|
||||
# Discriminator for program logs
|
||||
LOGS_CREATE_DISCRIMINATOR: Final[int] = 8530921459188068891
|
||||
|
||||
@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.
|
||||
|
||||
Args:
|
||||
logs: List of log strings from transaction
|
||||
signature: Transaction signature
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
# Check if this is a token creation
|
||||
if not any("Program log: Instruction: Create" in log for log in logs):
|
||||
return None
|
||||
|
||||
# Skip swaps and other operations
|
||||
if any("Program log: Instruction: CreateTokenAccount" in log for log in logs):
|
||||
return None
|
||||
|
||||
# Find and process program data
|
||||
for log in logs:
|
||||
if "Program data:" in log:
|
||||
try:
|
||||
encoded_data = log.split(": ")[1]
|
||||
decoded_data = base64.b64decode(encoded_data)
|
||||
return self._parse_create_instruction_data(decoded_data)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return None
|
||||
|
||||
def parse_token_creation_from_instruction(
|
||||
self,
|
||||
instruction_data: bytes,
|
||||
accounts: list[int],
|
||||
account_keys: list[bytes]
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from pump.fun instruction data.
|
||||
|
||||
Args:
|
||||
instruction_data: Raw instruction data
|
||||
accounts: List of account indices
|
||||
account_keys: List of account public keys
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
if not instruction_data.startswith(self.CREATE_DISCRIMINATOR_BYTES):
|
||||
return None
|
||||
|
||||
try:
|
||||
# Helper to get account key
|
||||
def get_account_key(index):
|
||||
if index >= len(accounts):
|
||||
return None
|
||||
account_index = accounts[index]
|
||||
if account_index >= len(account_keys):
|
||||
return None
|
||||
return Pubkey.from_bytes(account_keys[account_index])
|
||||
|
||||
# Parse instruction data
|
||||
token_data = self._parse_create_instruction_data(instruction_data)
|
||||
if not token_data:
|
||||
return None
|
||||
|
||||
# Extract account information
|
||||
mint = get_account_key(0)
|
||||
bonding_curve = get_account_key(2)
|
||||
associated_bonding_curve = get_account_key(3)
|
||||
user = get_account_key(7)
|
||||
|
||||
if not all([mint, bonding_curve, associated_bonding_curve, user]):
|
||||
return None
|
||||
|
||||
# Create creator vault
|
||||
creator = Pubkey.from_string(token_data["creator"]) if token_data.get("creator") else user
|
||||
creator_vault = self._derive_creator_vault(creator)
|
||||
|
||||
return TokenInfo(
|
||||
name=token_data["name"],
|
||||
symbol=token_data["symbol"],
|
||||
uri=token_data["uri"],
|
||||
mint=mint,
|
||||
platform=Platform.PUMP_FUN,
|
||||
bonding_curve=bonding_curve,
|
||||
associated_bonding_curve=associated_bonding_curve,
|
||||
user=user,
|
||||
creator=creator,
|
||||
creator_vault=creator_vault,
|
||||
creation_timestamp=monotonic(),
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def parse_token_creation_from_geyser(
|
||||
self,
|
||||
transaction_info: Any
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from Geyser transaction data.
|
||||
|
||||
Args:
|
||||
transaction_info: Geyser transaction information
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
try:
|
||||
if not hasattr(transaction_info, 'transaction'):
|
||||
return None
|
||||
|
||||
tx = transaction_info.transaction.transaction.transaction
|
||||
msg = getattr(tx, "message", None)
|
||||
if msg is None:
|
||||
return None
|
||||
|
||||
for ix in msg.instructions:
|
||||
# Skip non-pump.fun program instructions
|
||||
program_idx = ix.program_id_index
|
||||
if program_idx >= len(msg.account_keys):
|
||||
continue
|
||||
|
||||
program_id = msg.account_keys[program_idx]
|
||||
if bytes(program_id) != bytes(self.get_program_id()):
|
||||
continue
|
||||
|
||||
# Process instruction data
|
||||
token_info = self.parse_token_creation_from_instruction(
|
||||
ix.data, ix.accounts, msg.account_keys
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_program_id(self) -> Pubkey:
|
||||
"""Get the pump.fun program ID this parser monitors.
|
||||
|
||||
Returns:
|
||||
Pump.fun program ID
|
||||
"""
|
||||
return PumpAddresses.PROGRAM
|
||||
|
||||
def get_instruction_discriminators(self) -> list[bytes]:
|
||||
"""Get instruction discriminators for token creation.
|
||||
|
||||
Returns:
|
||||
List of discriminator bytes to match
|
||||
"""
|
||||
return [self.CREATE_DISCRIMINATOR_BYTES]
|
||||
|
||||
def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
|
||||
"""Parse token creation from block data (for block listener).
|
||||
|
||||
Args:
|
||||
block_data: Block data from WebSocket
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
try:
|
||||
if "transactions" not in block_data:
|
||||
return None
|
||||
|
||||
for tx in block_data["transactions"]:
|
||||
if not isinstance(tx, dict) or "transaction" not in tx:
|
||||
continue
|
||||
|
||||
# Decode base64 transaction data
|
||||
tx_data_encoded = tx["transaction"][0]
|
||||
tx_data_decoded = base64.b64decode(tx_data_encoded)
|
||||
transaction = VersionedTransaction.from_bytes(tx_data_decoded)
|
||||
|
||||
for ix in transaction.message.instructions:
|
||||
program_id = transaction.message.account_keys[ix.program_id_index]
|
||||
|
||||
# Check if instruction is from pump.fun program
|
||||
if str(program_id) != str(self.get_program_id()):
|
||||
continue
|
||||
|
||||
ix_data = bytes(ix.data)
|
||||
|
||||
# Check for create discriminator
|
||||
if len(ix_data) >= 8:
|
||||
discriminator = struct.unpack("<Q", ix_data[:8])[0]
|
||||
|
||||
if discriminator == self.CREATE_DISCRIMINATOR:
|
||||
# Token creation should have substantial data and many accounts
|
||||
if len(ix_data) <= 8 or len(ix.accounts) < 10:
|
||||
continue
|
||||
|
||||
# Parse the instruction
|
||||
token_info = self.parse_token_creation_from_instruction(
|
||||
ix_data, ix.accounts, transaction.message.account_keys
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _parse_create_instruction_data(self, data: bytes) -> dict | None:
|
||||
"""Parse the create instruction data from pump.fun.
|
||||
|
||||
Args:
|
||||
data: Raw instruction data
|
||||
|
||||
Returns:
|
||||
Dictionary of parsed data or None if parsing fails
|
||||
"""
|
||||
if len(data) < 8:
|
||||
return None
|
||||
|
||||
# Check for the correct instruction discriminator
|
||||
discriminator = struct.unpack("<Q", data[:8])[0]
|
||||
if discriminator not in [self.CREATE_DISCRIMINATOR, self.LOGS_CREATE_DISCRIMINATOR]:
|
||||
return None
|
||||
|
||||
offset = 8
|
||||
parsed_data = {}
|
||||
|
||||
try:
|
||||
# Parse fields based on CreateEvent structure
|
||||
fields = [
|
||||
("name", "string"),
|
||||
("symbol", "string"),
|
||||
("uri", "string"),
|
||||
]
|
||||
|
||||
# For instruction data, we also have creator info
|
||||
if discriminator == self.CREATE_DISCRIMINATOR:
|
||||
fields.extend([
|
||||
("creator", "publicKey"),
|
||||
])
|
||||
|
||||
for field_name, field_type in fields:
|
||||
if field_type == "string":
|
||||
if offset + 4 > len(data):
|
||||
return None
|
||||
length = struct.unpack("<I", data[offset : offset + 4])[0]
|
||||
offset += 4
|
||||
if offset + length > len(data):
|
||||
return None
|
||||
value = data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
elif field_type == "publicKey":
|
||||
if offset + 32 > len(data):
|
||||
return None
|
||||
value = base58.b58encode(data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
|
||||
parsed_data[field_name] = value
|
||||
|
||||
return parsed_data
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _derive_creator_vault(self, creator: Pubkey) -> Pubkey:
|
||||
"""Derive the creator vault for a creator.
|
||||
|
||||
Args:
|
||||
creator: Creator address
|
||||
|
||||
Returns:
|
||||
Creator vault address
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpAddresses.PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
|
||||
def _derive_associated_bonding_curve(self, mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
|
||||
"""Derive the associated bonding curve (ATA of bonding curve for the token).
|
||||
|
||||
Args:
|
||||
mint: Token mint address
|
||||
bonding_curve: Bonding curve address
|
||||
|
||||
Returns:
|
||||
Associated bonding curve address
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
bytes(bonding_curve),
|
||||
bytes(SystemAddresses.TOKEN_PROGRAM),
|
||||
bytes(mint),
|
||||
],
|
||||
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
Pump.Fun implementation of InstructionBuilder interface.
|
||||
|
||||
This module builds pump.fun-specific buy and sell instructions
|
||||
by implementing the InstructionBuilder interface.
|
||||
"""
|
||||
|
||||
import struct
|
||||
from typing import Final
|
||||
|
||||
from solders.instruction import AccountMeta, Instruction
|
||||
from solders.pubkey import Pubkey
|
||||
from spl.token.instructions import create_idempotent_associated_token_account
|
||||
|
||||
from core.pubkeys import TOKEN_DECIMALS, SystemAddresses
|
||||
from interfaces.core import AddressProvider, InstructionBuilder, Platform, TokenInfo
|
||||
|
||||
# Discriminators for pump.fun instructions
|
||||
BUY_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 16927863322537952870)
|
||||
SELL_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 12502976635542562355)
|
||||
|
||||
|
||||
class PumpFunInstructionBuilder(InstructionBuilder):
|
||||
"""Pump.Fun implementation of InstructionBuilder interface."""
|
||||
|
||||
@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 = (
|
||||
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 = (
|
||||
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
|
||||
+341
-21
@@ -1,5 +1,9 @@
|
||||
"""
|
||||
Base interfaces for trading operations.
|
||||
Enhanced base interfaces for trading operations with platform support.
|
||||
|
||||
This module provides the complete enhanced base classes that replace the existing
|
||||
trading/base.py while maintaining full backward compatibility. It integrates the
|
||||
new interface system with the existing trading infrastructure.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
@@ -8,13 +12,16 @@ from typing import Any
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from core.pubkeys import PumpAddresses
|
||||
from interfaces.core import Platform
|
||||
|
||||
# Import the new enhanced TokenInfo and Platform from interfaces
|
||||
from interfaces.core import TokenInfo as EnhancedTokenInfo
|
||||
|
||||
|
||||
# Keep the original TokenInfo structure for backward compatibility
|
||||
@dataclass
|
||||
class TokenInfo:
|
||||
"""Token information."""
|
||||
|
||||
class TokenInfo_Legacy:
|
||||
"""Legacy token information structure for backward compatibility."""
|
||||
name: str
|
||||
symbol: str
|
||||
uri: str
|
||||
@@ -26,14 +33,14 @@ class TokenInfo:
|
||||
creator_vault: Pubkey
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "TokenInfo":
|
||||
def from_dict(cls, data: dict[str, Any]) -> "TokenInfo_Legacy":
|
||||
"""Create TokenInfo from dictionary.
|
||||
|
||||
Args:
|
||||
data: Dictionary with token data
|
||||
|
||||
Returns:
|
||||
TokenInfo instance
|
||||
TokenInfo_Legacy instance
|
||||
"""
|
||||
return cls(
|
||||
name=data["name"],
|
||||
@@ -68,40 +75,353 @@ class TokenInfo:
|
||||
|
||||
@dataclass
|
||||
class TradeResult:
|
||||
"""Result of a trading operation."""
|
||||
|
||||
"""Enhanced result of a trading operation with platform support."""
|
||||
success: bool
|
||||
platform: Platform = Platform.PUMP_FUN # Add platform tracking
|
||||
tx_signature: str | None = None
|
||||
error_message: str | None = None
|
||||
amount: float | None = None
|
||||
price: float | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Convert to dictionary for logging/serialization.
|
||||
|
||||
Returns:
|
||||
Dictionary representation of the trade result
|
||||
"""
|
||||
return {
|
||||
"success": self.success,
|
||||
"platform": self.platform.value,
|
||||
"tx_signature": self.tx_signature,
|
||||
"error_message": self.error_message,
|
||||
"amount": self.amount,
|
||||
"price": self.price,
|
||||
}
|
||||
|
||||
|
||||
class Trader(ABC):
|
||||
"""Base interface for trading operations."""
|
||||
"""Enhanced base interface for trading operations with platform support."""
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, *args, **kwargs) -> TradeResult:
|
||||
async def execute(self, token_info: "TokenInfo", *args, **kwargs) -> TradeResult:
|
||||
"""Execute trading operation.
|
||||
|
||||
Args:
|
||||
token_info: Enhanced token information with platform support
|
||||
|
||||
Returns:
|
||||
TradeResult with operation outcome
|
||||
TradeResult with operation outcome including platform info
|
||||
"""
|
||||
pass
|
||||
|
||||
def _get_relevant_accounts(self, token_info: TokenInfo) -> list[Pubkey]:
|
||||
def _get_relevant_accounts(self, token_info: "TokenInfo") -> list[Pubkey]:
|
||||
"""
|
||||
Get the list of accounts relevant for calculating the priority fee.
|
||||
|
||||
This is now platform-agnostic and should be overridden by platform-specific traders.
|
||||
|
||||
Args:
|
||||
token_info: Token information for the buy/sell operation.
|
||||
token_info: Enhanced token information
|
||||
|
||||
Returns:
|
||||
list[Pubkey]: List of relevant accounts.
|
||||
List of relevant accounts (default implementation for pump.fun compatibility)
|
||||
"""
|
||||
return [
|
||||
token_info.mint, # Token mint address
|
||||
token_info.bonding_curve, # Bonding curve address
|
||||
PumpAddresses.PROGRAM, # Pump.fun program address
|
||||
PumpAddresses.FEE, # Pump.fun fee account
|
||||
]
|
||||
# Default implementation maintains pump.fun compatibility
|
||||
from core.pubkeys import PumpAddresses
|
||||
|
||||
accounts = [token_info.mint]
|
||||
|
||||
if token_info.bonding_curve:
|
||||
accounts.append(token_info.bonding_curve)
|
||||
|
||||
if token_info.pool_state: # For other platforms
|
||||
accounts.append(token_info.pool_state)
|
||||
|
||||
# Add platform program
|
||||
if token_info.platform == Platform.PUMP_FUN:
|
||||
accounts.extend([
|
||||
PumpAddresses.PROGRAM,
|
||||
PumpAddresses.FEE,
|
||||
])
|
||||
# Other platforms would add their specific accounts here
|
||||
|
||||
return accounts
|
||||
|
||||
|
||||
# Use the enhanced TokenInfo as the main TokenInfo class
|
||||
# This provides the new functionality while maintaining the same import path
|
||||
TokenInfo = EnhancedTokenInfo
|
||||
|
||||
|
||||
def upgrade_token_info(legacy_token_info: TokenInfo_Legacy) -> TokenInfo:
|
||||
"""Convert legacy TokenInfo to enhanced TokenInfo.
|
||||
|
||||
This function allows existing code that creates legacy TokenInfo objects
|
||||
to be upgraded to the new enhanced format.
|
||||
|
||||
Args:
|
||||
legacy_token_info: Legacy TokenInfo instance
|
||||
|
||||
Returns:
|
||||
Enhanced TokenInfo with platform information
|
||||
"""
|
||||
return TokenInfo(
|
||||
name=legacy_token_info.name,
|
||||
symbol=legacy_token_info.symbol,
|
||||
uri=legacy_token_info.uri,
|
||||
mint=legacy_token_info.mint,
|
||||
platform=Platform.PUMP_FUN, # Default to pump.fun for legacy tokens
|
||||
bonding_curve=legacy_token_info.bonding_curve,
|
||||
associated_bonding_curve=legacy_token_info.associated_bonding_curve,
|
||||
user=legacy_token_info.user,
|
||||
creator=legacy_token_info.creator,
|
||||
creator_vault=legacy_token_info.creator_vault,
|
||||
)
|
||||
|
||||
|
||||
def create_legacy_token_info(enhanced_token_info: TokenInfo) -> TokenInfo_Legacy:
|
||||
"""Convert enhanced TokenInfo back to legacy TokenInfo if needed.
|
||||
|
||||
This function allows the enhanced TokenInfo to be used with existing
|
||||
code that expects the legacy format.
|
||||
|
||||
Args:
|
||||
enhanced_token_info: Enhanced TokenInfo instance
|
||||
|
||||
Returns:
|
||||
Legacy TokenInfo instance
|
||||
|
||||
Raises:
|
||||
ValueError: If enhanced TokenInfo doesn't have required pump.fun fields
|
||||
"""
|
||||
if enhanced_token_info.platform != Platform.PUMP_FUN:
|
||||
raise ValueError("Can only convert pump.fun tokens to legacy format")
|
||||
|
||||
if not all([
|
||||
enhanced_token_info.bonding_curve,
|
||||
enhanced_token_info.associated_bonding_curve,
|
||||
enhanced_token_info.creator_vault
|
||||
]):
|
||||
raise ValueError("Enhanced TokenInfo missing required pump.fun fields")
|
||||
|
||||
return TokenInfo_Legacy(
|
||||
name=enhanced_token_info.name,
|
||||
symbol=enhanced_token_info.symbol,
|
||||
uri=enhanced_token_info.uri,
|
||||
mint=enhanced_token_info.mint,
|
||||
bonding_curve=enhanced_token_info.bonding_curve,
|
||||
associated_bonding_curve=enhanced_token_info.associated_bonding_curve,
|
||||
user=enhanced_token_info.user or enhanced_token_info.creator,
|
||||
creator=enhanced_token_info.creator or enhanced_token_info.user,
|
||||
creator_vault=enhanced_token_info.creator_vault,
|
||||
)
|
||||
|
||||
|
||||
def create_pump_fun_token_info(
|
||||
name: str,
|
||||
symbol: str,
|
||||
uri: str,
|
||||
mint: Pubkey,
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
user: Pubkey,
|
||||
creator: Pubkey | None = None,
|
||||
creator_vault: Pubkey | None = None,
|
||||
**kwargs
|
||||
) -> TokenInfo:
|
||||
"""Convenience function to create pump.fun TokenInfo with proper platform setting.
|
||||
|
||||
Args:
|
||||
name: Token name
|
||||
symbol: Token symbol
|
||||
uri: Token metadata URI
|
||||
mint: Token mint address
|
||||
bonding_curve: Bonding curve address
|
||||
associated_bonding_curve: Associated bonding curve address
|
||||
user: User/trader address
|
||||
creator: Creator address (defaults to user if not provided)
|
||||
creator_vault: Creator vault address (will be derived if not provided)
|
||||
**kwargs: Additional fields for TokenInfo
|
||||
|
||||
Returns:
|
||||
Enhanced TokenInfo configured for pump.fun
|
||||
"""
|
||||
from core.pubkeys import PumpAddresses
|
||||
|
||||
# Default creator to user if not provided
|
||||
if creator is None:
|
||||
creator = user
|
||||
|
||||
# Derive creator vault if not provided
|
||||
if creator_vault is None:
|
||||
creator_vault, _ = Pubkey.find_program_address(
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpAddresses.PROGRAM,
|
||||
)
|
||||
|
||||
return TokenInfo(
|
||||
name=name,
|
||||
symbol=symbol,
|
||||
uri=uri,
|
||||
mint=mint,
|
||||
platform=Platform.PUMP_FUN,
|
||||
bonding_curve=bonding_curve,
|
||||
associated_bonding_curve=associated_bonding_curve,
|
||||
user=user,
|
||||
creator=creator,
|
||||
creator_vault=creator_vault,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def create_lets_bonk_token_info(
|
||||
name: str,
|
||||
symbol: str,
|
||||
uri: str,
|
||||
mint: Pubkey,
|
||||
pool_state: Pubkey,
|
||||
base_vault: Pubkey,
|
||||
quote_vault: Pubkey,
|
||||
user: Pubkey,
|
||||
creator: Pubkey | None = None,
|
||||
**kwargs
|
||||
) -> TokenInfo:
|
||||
"""Convenience function to create LetsBonk TokenInfo with proper platform setting.
|
||||
|
||||
Args:
|
||||
name: Token name
|
||||
symbol: Token symbol
|
||||
uri: Token metadata URI
|
||||
mint: Token mint address
|
||||
pool_state: Pool state address
|
||||
base_vault: Base token vault address
|
||||
quote_vault: Quote token vault address
|
||||
user: User/trader address
|
||||
creator: Creator address (defaults to user if not provided)
|
||||
**kwargs: Additional fields for TokenInfo
|
||||
|
||||
Returns:
|
||||
Enhanced TokenInfo configured for LetsBonk
|
||||
"""
|
||||
# Default creator to user if not provided
|
||||
if creator is None:
|
||||
creator = user
|
||||
|
||||
return TokenInfo(
|
||||
name=name,
|
||||
symbol=symbol,
|
||||
uri=uri,
|
||||
mint=mint,
|
||||
platform=Platform.LETS_BONK,
|
||||
pool_state=pool_state,
|
||||
base_vault=base_vault,
|
||||
quote_vault=quote_vault,
|
||||
user=user,
|
||||
creator=creator,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def is_pump_fun_token(token_info: TokenInfo) -> bool:
|
||||
"""Check if a TokenInfo is for pump.fun platform.
|
||||
|
||||
Args:
|
||||
token_info: Token information to check
|
||||
|
||||
Returns:
|
||||
True if token is for pump.fun platform
|
||||
"""
|
||||
return token_info.platform == Platform.PUMP_FUN
|
||||
|
||||
|
||||
def is_lets_bonk_token(token_info: TokenInfo) -> bool:
|
||||
"""Check if a TokenInfo is for LetsBonk platform.
|
||||
|
||||
Args:
|
||||
token_info: Token information to check
|
||||
|
||||
Returns:
|
||||
True if token is for LetsBonk platform
|
||||
"""
|
||||
return token_info.platform == Platform.LETS_BONK
|
||||
|
||||
|
||||
def get_platform_specific_fields(token_info: TokenInfo) -> dict[str, Any]:
|
||||
"""Get platform-specific fields from TokenInfo.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
|
||||
Returns:
|
||||
Dictionary of platform-specific fields
|
||||
"""
|
||||
if token_info.platform == Platform.PUMP_FUN:
|
||||
return {
|
||||
"bonding_curve": token_info.bonding_curve,
|
||||
"associated_bonding_curve": token_info.associated_bonding_curve,
|
||||
"creator_vault": token_info.creator_vault,
|
||||
}
|
||||
elif token_info.platform == Platform.LETS_BONK:
|
||||
return {
|
||||
"pool_state": token_info.pool_state,
|
||||
"base_vault": token_info.base_vault,
|
||||
"quote_vault": token_info.quote_vault,
|
||||
}
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def validate_token_info(token_info: TokenInfo) -> bool:
|
||||
"""Validate that TokenInfo has required fields for its platform.
|
||||
|
||||
Args:
|
||||
token_info: Token information to validate
|
||||
|
||||
Returns:
|
||||
True if TokenInfo is valid for its platform
|
||||
"""
|
||||
# Check common required fields
|
||||
if not all([
|
||||
token_info.name,
|
||||
token_info.symbol,
|
||||
token_info.mint,
|
||||
token_info.platform,
|
||||
]):
|
||||
return False
|
||||
|
||||
# Check platform-specific required fields
|
||||
if token_info.platform == Platform.PUMP_FUN:
|
||||
return all([
|
||||
token_info.bonding_curve,
|
||||
token_info.associated_bonding_curve,
|
||||
])
|
||||
elif token_info.platform == Platform.LETS_BONK:
|
||||
return all([
|
||||
token_info.pool_state,
|
||||
token_info.base_vault,
|
||||
token_info.quote_vault,
|
||||
])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# Backward compatibility exports
|
||||
# This allows existing imports to continue working
|
||||
__all__ = [
|
||||
'Platform', # Platform enum
|
||||
'TokenInfo', # Enhanced TokenInfo (main export)
|
||||
'TokenInfo_Legacy', # Legacy TokenInfo for compatibility
|
||||
'TradeResult', # Enhanced TradeResult
|
||||
'Trader', # Enhanced Trader base class
|
||||
'create_legacy_token_info',
|
||||
'create_lets_bonk_token_info',
|
||||
# Convenience functions
|
||||
'create_pump_fun_token_info',
|
||||
'get_platform_specific_fields',
|
||||
'is_lets_bonk_token',
|
||||
# Utility functions
|
||||
'is_pump_fun_token',
|
||||
# Conversion functions
|
||||
'upgrade_token_info',
|
||||
'validate_token_info',
|
||||
]
|
||||
@@ -0,0 +1,315 @@
|
||||
"""
|
||||
Platform-aware trader implementations that use the interface system.
|
||||
|
||||
This module provides new trader classes that work with any platform
|
||||
through the interface system, while maintaining compatibility with existing code.
|
||||
"""
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from core.client import SolanaClient
|
||||
from core.priority_fee.manager import PriorityFeeManager
|
||||
from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
|
||||
from core.wallet import Wallet
|
||||
from interfaces.core import AddressProvider, Platform, TokenInfo
|
||||
from platforms import get_platform_implementations
|
||||
from trading.base import Trader, TradeResult
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PlatformAwareBuyer(Trader):
|
||||
"""Platform-aware token buyer that works with any supported platform."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: SolanaClient,
|
||||
wallet: Wallet,
|
||||
priority_fee_manager: PriorityFeeManager,
|
||||
amount: float,
|
||||
slippage: float = 0.01,
|
||||
max_retries: int = 5,
|
||||
extreme_fast_token_amount: int = 0,
|
||||
extreme_fast_mode: bool = False,
|
||||
):
|
||||
"""Initialize platform-aware token buyer.
|
||||
|
||||
Args:
|
||||
client: Solana client for RPC calls
|
||||
wallet: Wallet for signing transactions
|
||||
priority_fee_manager: Priority fee manager
|
||||
amount: Amount of SOL to spend
|
||||
slippage: Slippage tolerance (0.01 = 1%)
|
||||
max_retries: Maximum number of retry attempts
|
||||
extreme_fast_token_amount: Amount of token to buy if extreme fast mode is enabled
|
||||
extreme_fast_mode: If enabled, avoid fetching pool state for price estimation
|
||||
"""
|
||||
self.client = client
|
||||
self.wallet = wallet
|
||||
self.priority_fee_manager = priority_fee_manager
|
||||
self.amount = amount
|
||||
self.slippage = slippage
|
||||
self.max_retries = max_retries
|
||||
self.extreme_fast_mode = extreme_fast_mode
|
||||
self.extreme_fast_token_amount = extreme_fast_token_amount
|
||||
|
||||
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult:
|
||||
"""Execute buy operation using platform-specific implementations.
|
||||
|
||||
Args:
|
||||
token_info: Enhanced token information with platform
|
||||
|
||||
Returns:
|
||||
TradeResult with buy outcome
|
||||
"""
|
||||
try:
|
||||
# Get platform-specific implementations
|
||||
implementations = get_platform_implementations(token_info.platform, self.client)
|
||||
address_provider = implementations.address_provider
|
||||
instruction_builder = implementations.instruction_builder
|
||||
curve_manager = implementations.curve_manager
|
||||
|
||||
# Convert amount to lamports
|
||||
amount_lamports = int(self.amount * LAMPORTS_PER_SOL)
|
||||
|
||||
if self.extreme_fast_mode:
|
||||
# Skip the wait and directly calculate the amount
|
||||
token_amount = self.extreme_fast_token_amount
|
||||
token_price_sol = self.amount / token_amount if token_amount > 0 else 0
|
||||
else:
|
||||
# Get pool address based on platform
|
||||
pool_address = self._get_pool_address(token_info, address_provider)
|
||||
|
||||
# Regular behavior with RPC call
|
||||
token_price_sol = await curve_manager.calculate_price(pool_address)
|
||||
token_amount = self.amount / token_price_sol if token_price_sol > 0 else 0
|
||||
|
||||
# Calculate minimum token amount with slippage
|
||||
minimum_token_amount = token_amount * (1 - self.slippage)
|
||||
minimum_token_amount_raw = int(minimum_token_amount * 10**TOKEN_DECIMALS)
|
||||
|
||||
# Calculate maximum SOL to spend with slippage
|
||||
max_amount_lamports = int(amount_lamports * (1 + self.slippage))
|
||||
|
||||
# Build buy instructions
|
||||
instructions = await instruction_builder.build_buy_instruction(
|
||||
token_info,
|
||||
self.wallet.pubkey,
|
||||
max_amount_lamports, # amount_in (SOL)
|
||||
minimum_token_amount_raw, # minimum_amount_out (tokens)
|
||||
address_provider
|
||||
)
|
||||
|
||||
# Get accounts for priority fee calculation
|
||||
priority_accounts = instruction_builder.get_required_accounts_for_buy(
|
||||
token_info, self.wallet.pubkey, address_provider
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token"
|
||||
)
|
||||
logger.info(
|
||||
f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)"
|
||||
)
|
||||
|
||||
# Send transaction
|
||||
tx_signature = await self.client.build_and_send_transaction(
|
||||
instructions,
|
||||
self.wallet.keypair,
|
||||
skip_preflight=True,
|
||||
max_retries=self.max_retries,
|
||||
priority_fee=await self.priority_fee_manager.calculate_priority_fee(
|
||||
priority_accounts
|
||||
),
|
||||
)
|
||||
|
||||
success = await self.client.confirm_transaction(tx_signature)
|
||||
|
||||
if success:
|
||||
logger.info(f"Buy transaction confirmed: {tx_signature}")
|
||||
return TradeResult(
|
||||
success=True,
|
||||
platform=token_info.platform,
|
||||
tx_signature=tx_signature,
|
||||
amount=token_amount,
|
||||
price=token_price_sol,
|
||||
)
|
||||
else:
|
||||
return TradeResult(
|
||||
success=False,
|
||||
platform=token_info.platform,
|
||||
error_message=f"Transaction failed to confirm: {tx_signature}",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Buy operation failed: {e!s}")
|
||||
return TradeResult(
|
||||
success=False,
|
||||
platform=token_info.platform,
|
||||
error_message=str(e)
|
||||
)
|
||||
|
||||
def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey:
|
||||
"""Get the pool/curve address for price calculations.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
Pool/curve address
|
||||
"""
|
||||
if token_info.platform == Platform.PUMP_FUN:
|
||||
return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint)
|
||||
elif token_info.platform == Platform.LETS_BONK:
|
||||
return token_info.pool_state or address_provider.derive_pool_address(token_info.mint)
|
||||
else:
|
||||
# Fallback to deriving the address
|
||||
return address_provider.derive_pool_address(token_info.mint)
|
||||
|
||||
|
||||
class PlatformAwareSeller(Trader):
|
||||
"""Platform-aware token seller that works with any supported platform."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: SolanaClient,
|
||||
wallet: Wallet,
|
||||
priority_fee_manager: PriorityFeeManager,
|
||||
slippage: float = 0.25,
|
||||
max_retries: int = 5,
|
||||
):
|
||||
"""Initialize platform-aware token seller.
|
||||
|
||||
Args:
|
||||
client: Solana client for RPC calls
|
||||
wallet: Wallet for signing transactions
|
||||
priority_fee_manager: Priority fee manager
|
||||
slippage: Slippage tolerance (0.25 = 25%)
|
||||
max_retries: Maximum number of retry attempts
|
||||
"""
|
||||
self.client = client
|
||||
self.wallet = wallet
|
||||
self.priority_fee_manager = priority_fee_manager
|
||||
self.slippage = slippage
|
||||
self.max_retries = max_retries
|
||||
|
||||
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult:
|
||||
"""Execute sell operation using platform-specific implementations.
|
||||
|
||||
Args:
|
||||
token_info: Enhanced token information with platform
|
||||
|
||||
Returns:
|
||||
TradeResult with sell outcome
|
||||
"""
|
||||
try:
|
||||
# Get platform-specific implementations
|
||||
implementations = get_platform_implementations(token_info.platform, self.client)
|
||||
address_provider = implementations.address_provider
|
||||
instruction_builder = implementations.instruction_builder
|
||||
curve_manager = implementations.curve_manager
|
||||
|
||||
# Get user's token account and balance
|
||||
user_token_account = address_provider.derive_user_token_account(
|
||||
self.wallet.pubkey, token_info.mint
|
||||
)
|
||||
|
||||
token_balance = await self.client.get_token_account_balance(user_token_account)
|
||||
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
|
||||
|
||||
logger.info(f"Token balance: {token_balance_decimal}")
|
||||
|
||||
if token_balance == 0:
|
||||
logger.info("No tokens to sell.")
|
||||
return TradeResult(
|
||||
success=False,
|
||||
platform=token_info.platform,
|
||||
error_message="No tokens to sell"
|
||||
)
|
||||
|
||||
# Get pool address and current price
|
||||
pool_address = self._get_pool_address(token_info, address_provider)
|
||||
token_price_sol = await curve_manager.calculate_price(pool_address)
|
||||
|
||||
logger.info(f"Price per Token: {token_price_sol:.8f} SOL")
|
||||
|
||||
# Calculate minimum SOL output with slippage
|
||||
expected_sol_output = float(token_balance_decimal) * float(token_price_sol)
|
||||
min_sol_output = int((expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL)
|
||||
|
||||
logger.info(f"Selling {token_balance_decimal} tokens")
|
||||
logger.info(f"Expected SOL output: {expected_sol_output:.8f} SOL")
|
||||
logger.info(
|
||||
f"Minimum SOL output (with {self.slippage * 100}% slippage): {min_sol_output / LAMPORTS_PER_SOL:.8f} SOL"
|
||||
)
|
||||
|
||||
# Build sell instructions
|
||||
instructions = await instruction_builder.build_sell_instruction(
|
||||
token_info,
|
||||
self.wallet.pubkey,
|
||||
token_balance, # amount_in (tokens)
|
||||
min_sol_output, # minimum_amount_out (SOL)
|
||||
address_provider
|
||||
)
|
||||
|
||||
# Get accounts for priority fee calculation
|
||||
priority_accounts = instruction_builder.get_required_accounts_for_sell(
|
||||
token_info, self.wallet.pubkey, address_provider
|
||||
)
|
||||
|
||||
# Send transaction
|
||||
tx_signature = await self.client.build_and_send_transaction(
|
||||
instructions,
|
||||
self.wallet.keypair,
|
||||
skip_preflight=True,
|
||||
max_retries=self.max_retries,
|
||||
priority_fee=await self.priority_fee_manager.calculate_priority_fee(
|
||||
priority_accounts
|
||||
),
|
||||
)
|
||||
|
||||
success = await self.client.confirm_transaction(tx_signature)
|
||||
|
||||
if success:
|
||||
logger.info(f"Sell transaction confirmed: {tx_signature}")
|
||||
return TradeResult(
|
||||
success=True,
|
||||
platform=token_info.platform,
|
||||
tx_signature=tx_signature,
|
||||
amount=token_balance_decimal,
|
||||
price=token_price_sol,
|
||||
)
|
||||
else:
|
||||
return TradeResult(
|
||||
success=False,
|
||||
platform=token_info.platform,
|
||||
error_message=f"Transaction failed to confirm: {tx_signature}",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Sell operation failed: {e!s}")
|
||||
return TradeResult(
|
||||
success=False,
|
||||
platform=token_info.platform,
|
||||
error_message=str(e)
|
||||
)
|
||||
|
||||
def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey:
|
||||
"""Get the pool/curve address for price calculations.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
address_provider: Platform address provider
|
||||
|
||||
Returns:
|
||||
Pool/curve address
|
||||
"""
|
||||
if token_info.platform == Platform.PUMP_FUN:
|
||||
return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint)
|
||||
elif token_info.platform == Platform.LETS_BONK:
|
||||
return token_info.pool_state or address_provider.derive_pool_address(token_info.mint)
|
||||
else:
|
||||
# Fallback to deriving the address
|
||||
return address_provider.derive_pool_address(token_info.mint)
|
||||
Reference in New Issue
Block a user