mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-07-27 15:27:44 +00:00
docs(cursor): add uv rules, formatting
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
---
|
||||
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.
|
||||
@@ -20,6 +20,12 @@ The main codebase is organized as follows:
|
||||
- @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
|
||||
@@ -90,7 +96,7 @@ The main codebase is organized as follows:
|
||||
## Development Flow
|
||||
|
||||
1. Configure bot parameters in @bots/*.yaml
|
||||
2. Launch bot using @src/bot_runner.py
|
||||
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
|
||||
|
||||
@@ -106,4 +112,4 @@ The main codebase is organized as follows:
|
||||
|
||||
- @README.md: Project overview and setup instructions
|
||||
- @MAINTAINERS.md: Maintenance guidelines and procedures
|
||||
- @CLAUDE.md: Integration with Claude AI for strategy development
|
||||
- @CLAUDE.md: Development rules for Claude Code
|
||||
|
||||
@@ -23,7 +23,7 @@ Each listener has a dedicated test script for standalone performance analysis:
|
||||
|
||||
```bash
|
||||
# Run Geyser listener test for 30 seconds
|
||||
python tests/test_geyser_listener.py
|
||||
uv run tests/test_geyser_listener.py
|
||||
```
|
||||
|
||||
### Logs Listener Test
|
||||
@@ -35,7 +35,7 @@ python tests/test_geyser_listener.py
|
||||
|
||||
```bash
|
||||
# Run logs listener test
|
||||
python tests/test_logs_listener.py
|
||||
uv run tests/test_logs_listener.py
|
||||
```
|
||||
|
||||
### Block Listener Test
|
||||
@@ -47,7 +47,7 @@ python tests/test_logs_listener.py
|
||||
|
||||
```bash
|
||||
# Run block listener test
|
||||
python tests/test_block_listener.py
|
||||
uv run tests/test_block_listener.py
|
||||
```
|
||||
|
||||
## Comparative Performance Analysis
|
||||
@@ -62,10 +62,10 @@ python tests/test_block_listener.py
|
||||
|
||||
```bash
|
||||
# Compare all listeners for 60 seconds
|
||||
python tests/compare_listeners.py 60
|
||||
uv run tests/compare_listeners.py 60
|
||||
|
||||
# Default 60-second comparison
|
||||
python tests/compare_listeners.py
|
||||
uv run tests/compare_listeners.py
|
||||
```
|
||||
|
||||
### Performance Metrics
|
||||
@@ -123,19 +123,19 @@ export GEYSER_API_TOKEN="your-api-token"
|
||||
### Quick Performance Check
|
||||
```bash
|
||||
# 30-second comparison of all listeners
|
||||
python tests/compare_listeners.py 30
|
||||
uv run tests/compare_listeners.py 30
|
||||
```
|
||||
|
||||
### Extended Performance Analysis
|
||||
```bash
|
||||
# 10-minute deep analysis
|
||||
python tests/compare_listeners.py 600
|
||||
uv run tests/compare_listeners.py 600
|
||||
```
|
||||
|
||||
### Individual Listener Validation
|
||||
```bash
|
||||
# Test specific listener implementation
|
||||
python tests/test_geyser_listener.py
|
||||
uv run tests/test_geyser_listener.py
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
+42
-37
@@ -16,94 +16,99 @@ from utils.logger import setup_file_logging
|
||||
def setup_logging(bot_name: str):
|
||||
"""
|
||||
Set up logging to file for a specific bot instance.
|
||||
|
||||
|
||||
Args:
|
||||
bot_name: Name of the bot for the log file
|
||||
"""
|
||||
log_dir = Path("logs")
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
log_filename = log_dir / f"{bot_name}_{timestamp}.log"
|
||||
|
||||
|
||||
setup_file_logging(str(log_filename))
|
||||
|
||||
|
||||
async def start_bot(config_path: str):
|
||||
"""
|
||||
Start a trading bot with the configuration from the specified path.
|
||||
|
||||
|
||||
Args:
|
||||
config_path: Path to the YAML configuration file
|
||||
"""
|
||||
cfg = load_bot_config(config_path)
|
||||
setup_logging(cfg["name"])
|
||||
print_config_summary(cfg)
|
||||
|
||||
|
||||
trader = PumpTrader(
|
||||
# Connection settings
|
||||
rpc_endpoint=cfg["rpc_endpoint"],
|
||||
wss_endpoint=cfg["wss_endpoint"],
|
||||
private_key=cfg["private_key"],
|
||||
|
||||
# Trade parameters
|
||||
buy_amount=cfg["trade"]["buy_amount"],
|
||||
buy_slippage=cfg["trade"]["buy_slippage"],
|
||||
sell_slippage=cfg["trade"]["sell_slippage"],
|
||||
|
||||
# Extreme fast mode settings
|
||||
extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False),
|
||||
extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30),
|
||||
|
||||
# Exit strategy configuration
|
||||
exit_strategy=cfg["trade"].get("exit_strategy", "time_based"),
|
||||
take_profit_percentage=cfg["trade"].get("take_profit_percentage"),
|
||||
stop_loss_percentage=cfg["trade"].get("stop_loss_percentage"),
|
||||
max_hold_time=cfg["trade"].get("max_hold_time"),
|
||||
price_check_interval=cfg["trade"].get("price_check_interval", 10),
|
||||
|
||||
# Listener configuration
|
||||
listener_type=cfg["filters"]["listener_type"],
|
||||
|
||||
# Geyser configuration (if applicable)
|
||||
geyser_endpoint=cfg.get("geyser", {}).get("endpoint"),
|
||||
geyser_api_token=cfg.get("geyser", {}).get("api_token"),
|
||||
geyser_auth_type=cfg.get("geyser", {}).get("auth_type"),
|
||||
|
||||
# PumpPortal configuration (if applicable)
|
||||
pumpportal_url=cfg.get("pumpportal", {}).get("url", "wss://pumpportal.fun/api/data"),
|
||||
|
||||
pumpportal_url=cfg.get("pumpportal", {}).get(
|
||||
"url", "wss://pumpportal.fun/api/data"
|
||||
),
|
||||
# Priority fee configuration
|
||||
enable_dynamic_priority_fee=cfg.get("priority_fees", {}).get("enable_dynamic", False),
|
||||
enable_fixed_priority_fee=cfg.get("priority_fees", {}).get("enable_fixed", True),
|
||||
enable_dynamic_priority_fee=cfg.get("priority_fees", {}).get(
|
||||
"enable_dynamic", False
|
||||
),
|
||||
enable_fixed_priority_fee=cfg.get("priority_fees", {}).get(
|
||||
"enable_fixed", True
|
||||
),
|
||||
fixed_priority_fee=cfg.get("priority_fees", {}).get("fixed_amount", 500000),
|
||||
extra_priority_fee=cfg.get("priority_fees", {}).get("extra_percentage", 0.0),
|
||||
hard_cap_prior_fee=cfg.get("priority_fees", {}).get("hard_cap", 500000),
|
||||
|
||||
# Retry and timeout settings
|
||||
max_retries=cfg.get("retries", {}).get("max_attempts", 10),
|
||||
wait_time_after_creation=cfg.get("retries", {}).get("wait_after_creation", 15),
|
||||
wait_time_after_buy=cfg.get("retries", {}).get("wait_after_buy", 15),
|
||||
wait_time_before_new_token=cfg.get("retries", {}).get("wait_before_new_token", 15),
|
||||
wait_time_before_new_token=cfg.get("retries", {}).get(
|
||||
"wait_before_new_token", 15
|
||||
),
|
||||
max_token_age=cfg.get("timing", {}).get("max_token_age", 0.001),
|
||||
token_wait_timeout=cfg.get("timing", {}).get("token_wait_timeout", 30),
|
||||
|
||||
# Cleanup settings
|
||||
cleanup_mode=cfg.get("cleanup", {}).get("mode", "disabled"),
|
||||
cleanup_force_close_with_burn=cfg.get("cleanup", {}).get("force_close_with_burn", False),
|
||||
cleanup_with_priority_fee=cfg.get("cleanup", {}).get("with_priority_fee", False),
|
||||
|
||||
cleanup_force_close_with_burn=cfg.get("cleanup", {}).get(
|
||||
"force_close_with_burn", False
|
||||
),
|
||||
cleanup_with_priority_fee=cfg.get("cleanup", {}).get(
|
||||
"with_priority_fee", False
|
||||
),
|
||||
# Trading filters
|
||||
match_string=cfg["filters"].get("match_string"),
|
||||
bro_address=cfg["filters"].get("bro_address"),
|
||||
marry_mode=cfg["filters"].get("marry_mode", False),
|
||||
yolo_mode=cfg["filters"].get("yolo_mode", False),
|
||||
)
|
||||
|
||||
|
||||
await trader.start()
|
||||
|
||||
|
||||
def run_bot_process(config_path):
|
||||
asyncio.run(start_bot(config_path))
|
||||
|
||||
|
||||
def run_all_bots():
|
||||
"""
|
||||
Run all bots defined in YAML files in the 'bots' directory.
|
||||
@@ -114,22 +119,22 @@ def run_all_bots():
|
||||
if not bot_dir.exists():
|
||||
logging.error(f"Bot directory '{bot_dir}' not found")
|
||||
return
|
||||
|
||||
|
||||
bot_files = list(bot_dir.glob("*.yaml"))
|
||||
if not bot_files:
|
||||
logging.error(f"No bot configuration files found in '{bot_dir}'")
|
||||
return
|
||||
|
||||
|
||||
logging.info(f"Found {len(bot_files)} bot configuration files")
|
||||
|
||||
|
||||
processes = []
|
||||
skipped_bots = 0
|
||||
|
||||
|
||||
for file in bot_files:
|
||||
try:
|
||||
cfg = load_bot_config(str(file))
|
||||
bot_name = cfg.get("name", file.stem)
|
||||
|
||||
|
||||
# Skip bots with enabled=False
|
||||
if not cfg.get("enabled", True):
|
||||
logging.info(f"Skipping disabled bot '{bot_name}'")
|
||||
@@ -139,20 +144,20 @@ def run_all_bots():
|
||||
if cfg.get("separate_process", False):
|
||||
logging.info(f"Starting bot '{bot_name}' in separate process")
|
||||
p = multiprocessing.Process(
|
||||
target=run_bot_process,
|
||||
args=(str(file),),
|
||||
name=f"bot-{bot_name}"
|
||||
target=run_bot_process, args=(str(file),), name=f"bot-{bot_name}"
|
||||
)
|
||||
p.start()
|
||||
processes.append(p)
|
||||
else:
|
||||
logging.info(f"Starting bot '{bot_name}' in main process")
|
||||
asyncio.run(start_bot(str(file)))
|
||||
asyncio.run(start_bot(str(file)))
|
||||
except Exception as e:
|
||||
logging.exception(f"Failed to start bot from {file}: {e}")
|
||||
|
||||
logging.info(f"Started {len(bot_files) - skipped_bots} bots, skipped {skipped_bots} disabled bots")
|
||||
|
||||
|
||||
logging.info(
|
||||
f"Started {len(bot_files) - skipped_bots} bots, skipped {skipped_bots} disabled bots"
|
||||
)
|
||||
|
||||
for p in processes:
|
||||
p.join()
|
||||
logging.info(f"Process {p.name} completed")
|
||||
@@ -161,11 +166,11 @@ def run_all_bots():
|
||||
def main() -> None:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
|
||||
|
||||
run_all_bots()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -14,6 +14,7 @@ logger = get_logger(__name__)
|
||||
|
||||
class AccountCleanupManager:
|
||||
"""Handles safe cleanup of token accounts (ATA) after trading sessions."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: SolanaClient,
|
||||
@@ -60,7 +61,9 @@ class AccountCleanupManager:
|
||||
instructions = []
|
||||
|
||||
if balance > 0 and self.close_with_force_burn:
|
||||
logger.info(f"Burning {balance} tokens from ATA {ata} (mint: {mint})...")
|
||||
logger.info(
|
||||
f"Burning {balance} tokens from ATA {ata} (mint: {mint})..."
|
||||
)
|
||||
burn_ix = burn(
|
||||
BurnParams(
|
||||
account=ata,
|
||||
|
||||
+35
-9
@@ -17,26 +17,52 @@ def should_cleanup_post_session(cleanup_mode) -> bool:
|
||||
|
||||
|
||||
async def handle_cleanup_after_failure(
|
||||
client, wallet, mint, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn
|
||||
):
|
||||
client,
|
||||
wallet,
|
||||
mint,
|
||||
priority_fee_manager,
|
||||
cleanup_mode,
|
||||
cleanup_with_prior_fee,
|
||||
force_burn,
|
||||
):
|
||||
if should_cleanup_after_failure(cleanup_mode):
|
||||
logger.info("[Cleanup] Triggered by failed buy transaction.")
|
||||
manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn)
|
||||
manager = AccountCleanupManager(
|
||||
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
|
||||
)
|
||||
await manager.cleanup_ata(mint)
|
||||
|
||||
|
||||
async def handle_cleanup_after_sell(
|
||||
client, wallet, mint, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn
|
||||
):
|
||||
client,
|
||||
wallet,
|
||||
mint,
|
||||
priority_fee_manager,
|
||||
cleanup_mode,
|
||||
cleanup_with_prior_fee,
|
||||
force_burn,
|
||||
):
|
||||
if should_cleanup_after_sell(cleanup_mode):
|
||||
logger.info("[Cleanup] Triggered after token sell.")
|
||||
manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn)
|
||||
manager = AccountCleanupManager(
|
||||
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
|
||||
)
|
||||
await manager.cleanup_ata(mint)
|
||||
|
||||
|
||||
async def handle_cleanup_post_session(
|
||||
client, wallet, mints, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn
|
||||
):
|
||||
client,
|
||||
wallet,
|
||||
mints,
|
||||
priority_fee_manager,
|
||||
cleanup_mode,
|
||||
cleanup_with_prior_fee,
|
||||
force_burn,
|
||||
):
|
||||
if should_cleanup_post_session(cleanup_mode):
|
||||
logger.info("[Cleanup] Triggered post trading session.")
|
||||
manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn)
|
||||
manager = AccountCleanupManager(
|
||||
client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn
|
||||
)
|
||||
for mint in mints:
|
||||
await manager.cleanup_ata(mint)
|
||||
|
||||
+98
-43
@@ -5,48 +5,90 @@ import yaml
|
||||
from dotenv import load_dotenv
|
||||
|
||||
REQUIRED_FIELDS = [
|
||||
"name", "rpc_endpoint", "wss_endpoint", "private_key",
|
||||
"trade.buy_amount", "trade.buy_slippage", "trade.sell_slippage",
|
||||
"filters.listener_type", "filters.max_token_age"
|
||||
"name",
|
||||
"rpc_endpoint",
|
||||
"wss_endpoint",
|
||||
"private_key",
|
||||
"trade.buy_amount",
|
||||
"trade.buy_slippage",
|
||||
"trade.sell_slippage",
|
||||
"filters.listener_type",
|
||||
"filters.max_token_age",
|
||||
]
|
||||
|
||||
CONFIG_VALIDATION_RULES = [
|
||||
# (path, type, min_value, max_value, error_message)
|
||||
("trade.buy_amount", (int, float), 0, float('inf'), "trade.buy_amount must be a positive number"),
|
||||
(
|
||||
"trade.buy_amount",
|
||||
(int, float),
|
||||
0,
|
||||
float("inf"),
|
||||
"trade.buy_amount must be a positive number",
|
||||
),
|
||||
("trade.buy_slippage", float, 0, 1, "trade.buy_slippage must be between 0 and 1"),
|
||||
("trade.sell_slippage", float, 0, 1, "trade.sell_slippage must be between 0 and 1"),
|
||||
("priority_fees.fixed_amount", int, 0, float('inf'), "priority_fees.fixed_amount must be a non-negative integer"),
|
||||
("priority_fees.extra_percentage", float, 0, 1, "priority_fees.extra_percentage must be between 0 and 1"),
|
||||
("priority_fees.hard_cap", int, 0, float('inf'), "priority_fees.hard_cap must be a non-negative integer"),
|
||||
("retries.max_attempts", int, 0, 100, "retries.max_attempts must be between 0 and 100"),
|
||||
("filters.max_token_age", (int, float), 0, float('inf'), "filters.max_token_age must be a non-negative number")
|
||||
(
|
||||
"priority_fees.fixed_amount",
|
||||
int,
|
||||
0,
|
||||
float("inf"),
|
||||
"priority_fees.fixed_amount must be a non-negative integer",
|
||||
),
|
||||
(
|
||||
"priority_fees.extra_percentage",
|
||||
float,
|
||||
0,
|
||||
1,
|
||||
"priority_fees.extra_percentage must be between 0 and 1",
|
||||
),
|
||||
(
|
||||
"priority_fees.hard_cap",
|
||||
int,
|
||||
0,
|
||||
float("inf"),
|
||||
"priority_fees.hard_cap must be a non-negative integer",
|
||||
),
|
||||
(
|
||||
"retries.max_attempts",
|
||||
int,
|
||||
0,
|
||||
100,
|
||||
"retries.max_attempts must be between 0 and 100",
|
||||
),
|
||||
(
|
||||
"filters.max_token_age",
|
||||
(int, float),
|
||||
0,
|
||||
float("inf"),
|
||||
"filters.max_token_age must be a non-negative number",
|
||||
),
|
||||
]
|
||||
|
||||
# Valid values for enum-like fields
|
||||
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"]
|
||||
"trade.exit_strategy": ["time_based", "tp_sl", "manual"],
|
||||
}
|
||||
|
||||
|
||||
def load_bot_config(path: str) -> dict:
|
||||
"""
|
||||
Load and validate a bot configuration from a YAML file.
|
||||
|
||||
|
||||
Args:
|
||||
path: Path to the YAML configuration file (relative or absolute)
|
||||
|
||||
|
||||
Returns:
|
||||
Validated configuration dictionary
|
||||
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the configuration file doesn't exist
|
||||
ValueError: If the configuration is invalid
|
||||
"""
|
||||
with open(path) as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
|
||||
env_file = config.get("env_file")
|
||||
if env_file:
|
||||
env_path = os.path.join(os.path.dirname(path), env_file)
|
||||
@@ -55,19 +97,21 @@ def load_bot_config(path: str) -> dict:
|
||||
else:
|
||||
# If not found relative to config, try relative to current working directory
|
||||
load_dotenv(env_file, override=True)
|
||||
|
||||
|
||||
resolve_env_vars(config)
|
||||
validate_config(config)
|
||||
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def resolve_env_vars(config: dict) -> None:
|
||||
"""
|
||||
Recursively resolve environment variables in the configuration.
|
||||
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary to process
|
||||
"""
|
||||
|
||||
def resolve_env(value):
|
||||
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
|
||||
env_var = value[2:-1]
|
||||
@@ -76,27 +120,28 @@ def resolve_env_vars(config: dict) -> None:
|
||||
raise ValueError(f"Environment variable '{env_var}' not found")
|
||||
return env_value
|
||||
return value
|
||||
|
||||
|
||||
def resolve_all(d):
|
||||
for k, v in d.items():
|
||||
if isinstance(v, dict):
|
||||
resolve_all(v)
|
||||
else:
|
||||
d[k] = resolve_env(v)
|
||||
|
||||
|
||||
resolve_all(config)
|
||||
|
||||
|
||||
def get_nested_value(config: dict, path: str) -> Any:
|
||||
"""
|
||||
Get a nested value from the configuration using dot notation.
|
||||
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary
|
||||
path: Path to the value using dot notation (e.g., "trade.buy_amount")
|
||||
|
||||
|
||||
Returns:
|
||||
The value at the specified path
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If the path doesn't exist in the configuration
|
||||
"""
|
||||
@@ -108,36 +153,37 @@ def get_nested_value(config: dict, path: str) -> Any:
|
||||
value = value[key]
|
||||
return value
|
||||
|
||||
|
||||
def validate_config(config: dict) -> None:
|
||||
"""
|
||||
Validate the configuration against defined rules.
|
||||
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary to validate
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If the configuration is invalid
|
||||
"""
|
||||
for field in REQUIRED_FIELDS:
|
||||
get_nested_value(config, field)
|
||||
|
||||
|
||||
for path, expected_type, min_val, max_val, error_msg in CONFIG_VALIDATION_RULES:
|
||||
try:
|
||||
value = get_nested_value(config, path)
|
||||
|
||||
|
||||
if not isinstance(value, expected_type):
|
||||
raise ValueError(f"Type error: {error_msg}")
|
||||
|
||||
|
||||
if isinstance(value, (int, float)) and not (min_val <= value <= max_val):
|
||||
raise ValueError(f"Range error: {error_msg}")
|
||||
|
||||
|
||||
except ValueError as e:
|
||||
# Re-raise if it's our own error
|
||||
if str(e).startswith(("Type error:", "Range error:")):
|
||||
raise
|
||||
# Otherwise, the field might be missing
|
||||
continue
|
||||
|
||||
|
||||
# Validate enum-like fields
|
||||
for path, valid_values in VALID_VALUES.items():
|
||||
try:
|
||||
@@ -147,43 +193,52 @@ def validate_config(config: dict) -> None:
|
||||
except ValueError:
|
||||
# Skip if the field is missing
|
||||
continue
|
||||
|
||||
|
||||
# Cannot enable both dynamic and fixed priority fees
|
||||
try:
|
||||
dynamic = get_nested_value(config, "priority_fees.enable_dynamic")
|
||||
fixed = get_nested_value(config, "priority_fees.enable_fixed")
|
||||
if dynamic and fixed:
|
||||
raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously")
|
||||
raise ValueError(
|
||||
"Cannot enable both dynamic and fixed priority fees simultaneously"
|
||||
)
|
||||
except ValueError:
|
||||
# Skip if one of the fields is missing
|
||||
pass
|
||||
|
||||
|
||||
def print_config_summary(config: dict) -> None:
|
||||
"""
|
||||
Print a summary of the loaded configuration.
|
||||
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary
|
||||
"""
|
||||
print(f"Bot name: {config.get('name', 'unnamed')}")
|
||||
print(f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}")
|
||||
|
||||
trade = config.get('trade', {})
|
||||
print(
|
||||
f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}"
|
||||
)
|
||||
|
||||
trade = config.get("trade", {})
|
||||
print("Trade settings:")
|
||||
print(f" - Buy amount: {trade.get('buy_amount', 'not configured')} SOL")
|
||||
print(f" - Buy slippage: {trade.get('buy_slippage', 'not configured') * 100}%")
|
||||
print(f" - Extreme fast mode: {'enabled' if trade.get('extreme_fast_mode') else 'disabled'}")
|
||||
|
||||
fees = config.get('priority_fees', {})
|
||||
print(
|
||||
f" - Extreme fast mode: {'enabled' if trade.get('extreme_fast_mode') else 'disabled'}"
|
||||
)
|
||||
|
||||
fees = config.get("priority_fees", {})
|
||||
print("Priority fees:")
|
||||
if fees.get('enable_dynamic'):
|
||||
if fees.get("enable_dynamic"):
|
||||
print(" - Dynamic fees enabled")
|
||||
elif fees.get('enable_fixed'):
|
||||
print(f" - Fixed fee: {fees.get('fixed_amount', 'not configured')} microlamports")
|
||||
|
||||
elif fees.get("enable_fixed"):
|
||||
print(
|
||||
f" - Fixed fee: {fees.get('fixed_amount', 'not configured')} microlamports"
|
||||
)
|
||||
|
||||
print("Configuration loaded successfully!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = load_bot_config("bots/bot-sniper.yaml")
|
||||
print_config_summary(config)
|
||||
print_config_summary(config)
|
||||
|
||||
+9
-3
@@ -36,7 +36,9 @@ class SolanaClient:
|
||||
self._client = None
|
||||
self._cached_blockhash: Hash | None = None
|
||||
self._blockhash_lock = asyncio.Lock()
|
||||
self._blockhash_updater_task = asyncio.create_task(self.start_blockhash_updater())
|
||||
self._blockhash_updater_task = asyncio.create_task(
|
||||
self.start_blockhash_updater()
|
||||
)
|
||||
|
||||
async def start_blockhash_updater(self, interval: float = 5.0):
|
||||
"""Start background task to update recent blockhash."""
|
||||
@@ -104,7 +106,9 @@ class SolanaClient:
|
||||
ValueError: If account doesn't exist or has no data
|
||||
"""
|
||||
client = await self.get_client()
|
||||
response = await client.get_account_info(pubkey, encoding="base64") # base64 encoding for account data by default
|
||||
response = await client.get_account_info(
|
||||
pubkey, encoding="base64"
|
||||
) # base64 encoding for account data by default
|
||||
if not response.value:
|
||||
raise ValueError(f"Account {pubkey} not found")
|
||||
return response.value
|
||||
@@ -207,7 +211,9 @@ class SolanaClient:
|
||||
"""
|
||||
client = await self.get_client()
|
||||
try:
|
||||
await client.confirm_transaction(signature, commitment=commitment, sleep_seconds=1)
|
||||
await client.confirm_transaction(
|
||||
signature, commitment=commitment, sleep_seconds=1
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to confirm transaction {signature}: {e!s}")
|
||||
|
||||
+2
-2
@@ -45,9 +45,9 @@ class BondingCurveState:
|
||||
|
||||
parsed = self._STRUCT.parse(data[8:])
|
||||
self.__dict__.update(parsed)
|
||||
|
||||
|
||||
# Convert raw bytes to Pubkey for creator field
|
||||
if hasattr(self, 'creator') and isinstance(self.creator, bytes):
|
||||
if hasattr(self, "creator") and isinstance(self.creator, bytes):
|
||||
self.creator = Pubkey.from_bytes(self.creator)
|
||||
|
||||
def calculate_price(self) -> float:
|
||||
|
||||
+116
-115
File diff suppressed because one or more lines are too long
@@ -3,7 +3,13 @@ from google.protobuf.internal import containers as _containers
|
||||
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
from typing import (
|
||||
ClassVar as _ClassVar,
|
||||
Iterable as _Iterable,
|
||||
Mapping as _Mapping,
|
||||
Optional as _Optional,
|
||||
Union as _Union,
|
||||
)
|
||||
from solana_storage_pb2 import ConfirmedBlock as ConfirmedBlock
|
||||
from solana_storage_pb2 import ConfirmedTransaction as ConfirmedTransaction
|
||||
from solana_storage_pb2 import Transaction as Transaction
|
||||
@@ -41,6 +47,7 @@ class CommitmentLevel(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
COMPLETED: _ClassVar[CommitmentLevel]
|
||||
CREATED_BANK: _ClassVar[CommitmentLevel]
|
||||
DEAD: _ClassVar[CommitmentLevel]
|
||||
|
||||
PROCESSED: CommitmentLevel
|
||||
CONFIRMED: CommitmentLevel
|
||||
FINALIZED: CommitmentLevel
|
||||
@@ -50,56 +57,106 @@ CREATED_BANK: CommitmentLevel
|
||||
DEAD: CommitmentLevel
|
||||
|
||||
class SubscribeRequest(_message.Message):
|
||||
__slots__ = ("accounts", "slots", "transactions", "transactions_status", "blocks", "blocks_meta", "entry", "commitment", "accounts_data_slice", "ping")
|
||||
__slots__ = (
|
||||
"accounts",
|
||||
"slots",
|
||||
"transactions",
|
||||
"transactions_status",
|
||||
"blocks",
|
||||
"blocks_meta",
|
||||
"entry",
|
||||
"commitment",
|
||||
"accounts_data_slice",
|
||||
"ping",
|
||||
)
|
||||
class AccountsEntry(_message.Message):
|
||||
__slots__ = ("key", "value")
|
||||
KEY_FIELD_NUMBER: _ClassVar[int]
|
||||
VALUE_FIELD_NUMBER: _ClassVar[int]
|
||||
key: str
|
||||
value: SubscribeRequestFilterAccounts
|
||||
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterAccounts, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
key: _Optional[str] = ...,
|
||||
value: _Optional[_Union[SubscribeRequestFilterAccounts, _Mapping]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SlotsEntry(_message.Message):
|
||||
__slots__ = ("key", "value")
|
||||
KEY_FIELD_NUMBER: _ClassVar[int]
|
||||
VALUE_FIELD_NUMBER: _ClassVar[int]
|
||||
key: str
|
||||
value: SubscribeRequestFilterSlots
|
||||
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterSlots, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
key: _Optional[str] = ...,
|
||||
value: _Optional[_Union[SubscribeRequestFilterSlots, _Mapping]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class TransactionsEntry(_message.Message):
|
||||
__slots__ = ("key", "value")
|
||||
KEY_FIELD_NUMBER: _ClassVar[int]
|
||||
VALUE_FIELD_NUMBER: _ClassVar[int]
|
||||
key: str
|
||||
value: SubscribeRequestFilterTransactions
|
||||
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterTransactions, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
key: _Optional[str] = ...,
|
||||
value: _Optional[
|
||||
_Union[SubscribeRequestFilterTransactions, _Mapping]
|
||||
] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class TransactionsStatusEntry(_message.Message):
|
||||
__slots__ = ("key", "value")
|
||||
KEY_FIELD_NUMBER: _ClassVar[int]
|
||||
VALUE_FIELD_NUMBER: _ClassVar[int]
|
||||
key: str
|
||||
value: SubscribeRequestFilterTransactions
|
||||
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterTransactions, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
key: _Optional[str] = ...,
|
||||
value: _Optional[
|
||||
_Union[SubscribeRequestFilterTransactions, _Mapping]
|
||||
] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class BlocksEntry(_message.Message):
|
||||
__slots__ = ("key", "value")
|
||||
KEY_FIELD_NUMBER: _ClassVar[int]
|
||||
VALUE_FIELD_NUMBER: _ClassVar[int]
|
||||
key: str
|
||||
value: SubscribeRequestFilterBlocks
|
||||
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterBlocks, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
key: _Optional[str] = ...,
|
||||
value: _Optional[_Union[SubscribeRequestFilterBlocks, _Mapping]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class BlocksMetaEntry(_message.Message):
|
||||
__slots__ = ("key", "value")
|
||||
KEY_FIELD_NUMBER: _ClassVar[int]
|
||||
VALUE_FIELD_NUMBER: _ClassVar[int]
|
||||
key: str
|
||||
value: SubscribeRequestFilterBlocksMeta
|
||||
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterBlocksMeta, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
key: _Optional[str] = ...,
|
||||
value: _Optional[_Union[SubscribeRequestFilterBlocksMeta, _Mapping]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class EntryEntry(_message.Message):
|
||||
__slots__ = ("key", "value")
|
||||
KEY_FIELD_NUMBER: _ClassVar[int]
|
||||
VALUE_FIELD_NUMBER: _ClassVar[int]
|
||||
key: str
|
||||
value: SubscribeRequestFilterEntry
|
||||
def __init__(self, key: _Optional[str] = ..., value: _Optional[_Union[SubscribeRequestFilterEntry, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
key: _Optional[str] = ...,
|
||||
value: _Optional[_Union[SubscribeRequestFilterEntry, _Mapping]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
|
||||
SLOTS_FIELD_NUMBER: _ClassVar[int]
|
||||
TRANSACTIONS_FIELD_NUMBER: _ClassVar[int]
|
||||
@@ -118,9 +175,29 @@ class SubscribeRequest(_message.Message):
|
||||
blocks_meta: _containers.MessageMap[str, SubscribeRequestFilterBlocksMeta]
|
||||
entry: _containers.MessageMap[str, SubscribeRequestFilterEntry]
|
||||
commitment: CommitmentLevel
|
||||
accounts_data_slice: _containers.RepeatedCompositeFieldContainer[SubscribeRequestAccountsDataSlice]
|
||||
accounts_data_slice: _containers.RepeatedCompositeFieldContainer[
|
||||
SubscribeRequestAccountsDataSlice
|
||||
]
|
||||
ping: SubscribeRequestPing
|
||||
def __init__(self, accounts: _Optional[_Mapping[str, SubscribeRequestFilterAccounts]] = ..., slots: _Optional[_Mapping[str, SubscribeRequestFilterSlots]] = ..., transactions: _Optional[_Mapping[str, SubscribeRequestFilterTransactions]] = ..., transactions_status: _Optional[_Mapping[str, SubscribeRequestFilterTransactions]] = ..., blocks: _Optional[_Mapping[str, SubscribeRequestFilterBlocks]] = ..., blocks_meta: _Optional[_Mapping[str, SubscribeRequestFilterBlocksMeta]] = ..., entry: _Optional[_Mapping[str, SubscribeRequestFilterEntry]] = ..., commitment: _Optional[_Union[CommitmentLevel, str]] = ..., accounts_data_slice: _Optional[_Iterable[_Union[SubscribeRequestAccountsDataSlice, _Mapping]]] = ..., ping: _Optional[_Union[SubscribeRequestPing, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
accounts: _Optional[_Mapping[str, SubscribeRequestFilterAccounts]] = ...,
|
||||
slots: _Optional[_Mapping[str, SubscribeRequestFilterSlots]] = ...,
|
||||
transactions: _Optional[
|
||||
_Mapping[str, SubscribeRequestFilterTransactions]
|
||||
] = ...,
|
||||
transactions_status: _Optional[
|
||||
_Mapping[str, SubscribeRequestFilterTransactions]
|
||||
] = ...,
|
||||
blocks: _Optional[_Mapping[str, SubscribeRequestFilterBlocks]] = ...,
|
||||
blocks_meta: _Optional[_Mapping[str, SubscribeRequestFilterBlocksMeta]] = ...,
|
||||
entry: _Optional[_Mapping[str, SubscribeRequestFilterEntry]] = ...,
|
||||
commitment: _Optional[_Union[CommitmentLevel, str]] = ...,
|
||||
accounts_data_slice: _Optional[
|
||||
_Iterable[_Union[SubscribeRequestAccountsDataSlice, _Mapping]]
|
||||
] = ...,
|
||||
ping: _Optional[_Union[SubscribeRequestPing, _Mapping]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeRequestFilterAccounts(_message.Message):
|
||||
__slots__ = ("account", "owner", "filters", "nonempty_txn_signature")
|
||||
@@ -130,9 +207,19 @@ class SubscribeRequestFilterAccounts(_message.Message):
|
||||
NONEMPTY_TXN_SIGNATURE_FIELD_NUMBER: _ClassVar[int]
|
||||
account: _containers.RepeatedScalarFieldContainer[str]
|
||||
owner: _containers.RepeatedScalarFieldContainer[str]
|
||||
filters: _containers.RepeatedCompositeFieldContainer[SubscribeRequestFilterAccountsFilter]
|
||||
filters: _containers.RepeatedCompositeFieldContainer[
|
||||
SubscribeRequestFilterAccountsFilter
|
||||
]
|
||||
nonempty_txn_signature: bool
|
||||
def __init__(self, account: _Optional[_Iterable[str]] = ..., owner: _Optional[_Iterable[str]] = ..., filters: _Optional[_Iterable[_Union[SubscribeRequestFilterAccountsFilter, _Mapping]]] = ..., nonempty_txn_signature: bool = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
account: _Optional[_Iterable[str]] = ...,
|
||||
owner: _Optional[_Iterable[str]] = ...,
|
||||
filters: _Optional[
|
||||
_Iterable[_Union[SubscribeRequestFilterAccountsFilter, _Mapping]]
|
||||
] = ...,
|
||||
nonempty_txn_signature: bool = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeRequestFilterAccountsFilter(_message.Message):
|
||||
__slots__ = ("memcmp", "datasize", "token_account_state", "lamports")
|
||||
@@ -144,7 +231,17 @@ class SubscribeRequestFilterAccountsFilter(_message.Message):
|
||||
datasize: int
|
||||
token_account_state: bool
|
||||
lamports: SubscribeRequestFilterAccountsFilterLamports
|
||||
def __init__(self, memcmp: _Optional[_Union[SubscribeRequestFilterAccountsFilterMemcmp, _Mapping]] = ..., datasize: _Optional[int] = ..., token_account_state: bool = ..., lamports: _Optional[_Union[SubscribeRequestFilterAccountsFilterLamports, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
memcmp: _Optional[
|
||||
_Union[SubscribeRequestFilterAccountsFilterMemcmp, _Mapping]
|
||||
] = ...,
|
||||
datasize: _Optional[int] = ...,
|
||||
token_account_state: bool = ...,
|
||||
lamports: _Optional[
|
||||
_Union[SubscribeRequestFilterAccountsFilterLamports, _Mapping]
|
||||
] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeRequestFilterAccountsFilterMemcmp(_message.Message):
|
||||
__slots__ = ("offset", "bytes", "base58", "base64")
|
||||
@@ -156,7 +253,13 @@ class SubscribeRequestFilterAccountsFilterMemcmp(_message.Message):
|
||||
bytes: bytes
|
||||
base58: str
|
||||
base64: str
|
||||
def __init__(self, offset: _Optional[int] = ..., bytes: _Optional[bytes] = ..., base58: _Optional[str] = ..., base64: _Optional[str] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
offset: _Optional[int] = ...,
|
||||
bytes: _Optional[bytes] = ...,
|
||||
base58: _Optional[str] = ...,
|
||||
base64: _Optional[str] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeRequestFilterAccountsFilterLamports(_message.Message):
|
||||
__slots__ = ("eq", "ne", "lt", "gt")
|
||||
@@ -168,7 +271,13 @@ class SubscribeRequestFilterAccountsFilterLamports(_message.Message):
|
||||
ne: int
|
||||
lt: int
|
||||
gt: int
|
||||
def __init__(self, eq: _Optional[int] = ..., ne: _Optional[int] = ..., lt: _Optional[int] = ..., gt: _Optional[int] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
eq: _Optional[int] = ...,
|
||||
ne: _Optional[int] = ...,
|
||||
lt: _Optional[int] = ...,
|
||||
gt: _Optional[int] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeRequestFilterSlots(_message.Message):
|
||||
__slots__ = ("filter_by_commitment",)
|
||||
@@ -177,7 +286,14 @@ class SubscribeRequestFilterSlots(_message.Message):
|
||||
def __init__(self, filter_by_commitment: bool = ...) -> None: ...
|
||||
|
||||
class SubscribeRequestFilterTransactions(_message.Message):
|
||||
__slots__ = ("vote", "failed", "signature", "account_include", "account_exclude", "account_required")
|
||||
__slots__ = (
|
||||
"vote",
|
||||
"failed",
|
||||
"signature",
|
||||
"account_include",
|
||||
"account_exclude",
|
||||
"account_required",
|
||||
)
|
||||
VOTE_FIELD_NUMBER: _ClassVar[int]
|
||||
FAILED_FIELD_NUMBER: _ClassVar[int]
|
||||
SIGNATURE_FIELD_NUMBER: _ClassVar[int]
|
||||
@@ -190,10 +306,23 @@ class SubscribeRequestFilterTransactions(_message.Message):
|
||||
account_include: _containers.RepeatedScalarFieldContainer[str]
|
||||
account_exclude: _containers.RepeatedScalarFieldContainer[str]
|
||||
account_required: _containers.RepeatedScalarFieldContainer[str]
|
||||
def __init__(self, vote: bool = ..., failed: bool = ..., signature: _Optional[str] = ..., account_include: _Optional[_Iterable[str]] = ..., account_exclude: _Optional[_Iterable[str]] = ..., account_required: _Optional[_Iterable[str]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
vote: bool = ...,
|
||||
failed: bool = ...,
|
||||
signature: _Optional[str] = ...,
|
||||
account_include: _Optional[_Iterable[str]] = ...,
|
||||
account_exclude: _Optional[_Iterable[str]] = ...,
|
||||
account_required: _Optional[_Iterable[str]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeRequestFilterBlocks(_message.Message):
|
||||
__slots__ = ("account_include", "include_transactions", "include_accounts", "include_entries")
|
||||
__slots__ = (
|
||||
"account_include",
|
||||
"include_transactions",
|
||||
"include_accounts",
|
||||
"include_entries",
|
||||
)
|
||||
ACCOUNT_INCLUDE_FIELD_NUMBER: _ClassVar[int]
|
||||
INCLUDE_TRANSACTIONS_FIELD_NUMBER: _ClassVar[int]
|
||||
INCLUDE_ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
|
||||
@@ -202,7 +331,13 @@ class SubscribeRequestFilterBlocks(_message.Message):
|
||||
include_transactions: bool
|
||||
include_accounts: bool
|
||||
include_entries: bool
|
||||
def __init__(self, account_include: _Optional[_Iterable[str]] = ..., include_transactions: bool = ..., include_accounts: bool = ..., include_entries: bool = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
account_include: _Optional[_Iterable[str]] = ...,
|
||||
include_transactions: bool = ...,
|
||||
include_accounts: bool = ...,
|
||||
include_entries: bool = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeRequestFilterBlocksMeta(_message.Message):
|
||||
__slots__ = ()
|
||||
@@ -218,7 +353,9 @@ class SubscribeRequestAccountsDataSlice(_message.Message):
|
||||
LENGTH_FIELD_NUMBER: _ClassVar[int]
|
||||
offset: int
|
||||
length: int
|
||||
def __init__(self, offset: _Optional[int] = ..., length: _Optional[int] = ...) -> None: ...
|
||||
def __init__(
|
||||
self, offset: _Optional[int] = ..., length: _Optional[int] = ...
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeRequestPing(_message.Message):
|
||||
__slots__ = ("id",)
|
||||
@@ -227,7 +364,18 @@ class SubscribeRequestPing(_message.Message):
|
||||
def __init__(self, id: _Optional[int] = ...) -> None: ...
|
||||
|
||||
class SubscribeUpdate(_message.Message):
|
||||
__slots__ = ("filters", "account", "slot", "transaction", "transaction_status", "block", "ping", "pong", "block_meta", "entry")
|
||||
__slots__ = (
|
||||
"filters",
|
||||
"account",
|
||||
"slot",
|
||||
"transaction",
|
||||
"transaction_status",
|
||||
"block",
|
||||
"ping",
|
||||
"pong",
|
||||
"block_meta",
|
||||
"entry",
|
||||
)
|
||||
FILTERS_FIELD_NUMBER: _ClassVar[int]
|
||||
ACCOUNT_FIELD_NUMBER: _ClassVar[int]
|
||||
SLOT_FIELD_NUMBER: _ClassVar[int]
|
||||
@@ -248,7 +396,21 @@ class SubscribeUpdate(_message.Message):
|
||||
pong: SubscribeUpdatePong
|
||||
block_meta: SubscribeUpdateBlockMeta
|
||||
entry: SubscribeUpdateEntry
|
||||
def __init__(self, filters: _Optional[_Iterable[str]] = ..., account: _Optional[_Union[SubscribeUpdateAccount, _Mapping]] = ..., slot: _Optional[_Union[SubscribeUpdateSlot, _Mapping]] = ..., transaction: _Optional[_Union[SubscribeUpdateTransaction, _Mapping]] = ..., transaction_status: _Optional[_Union[SubscribeUpdateTransactionStatus, _Mapping]] = ..., block: _Optional[_Union[SubscribeUpdateBlock, _Mapping]] = ..., ping: _Optional[_Union[SubscribeUpdatePing, _Mapping]] = ..., pong: _Optional[_Union[SubscribeUpdatePong, _Mapping]] = ..., block_meta: _Optional[_Union[SubscribeUpdateBlockMeta, _Mapping]] = ..., entry: _Optional[_Union[SubscribeUpdateEntry, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
filters: _Optional[_Iterable[str]] = ...,
|
||||
account: _Optional[_Union[SubscribeUpdateAccount, _Mapping]] = ...,
|
||||
slot: _Optional[_Union[SubscribeUpdateSlot, _Mapping]] = ...,
|
||||
transaction: _Optional[_Union[SubscribeUpdateTransaction, _Mapping]] = ...,
|
||||
transaction_status: _Optional[
|
||||
_Union[SubscribeUpdateTransactionStatus, _Mapping]
|
||||
] = ...,
|
||||
block: _Optional[_Union[SubscribeUpdateBlock, _Mapping]] = ...,
|
||||
ping: _Optional[_Union[SubscribeUpdatePing, _Mapping]] = ...,
|
||||
pong: _Optional[_Union[SubscribeUpdatePong, _Mapping]] = ...,
|
||||
block_meta: _Optional[_Union[SubscribeUpdateBlockMeta, _Mapping]] = ...,
|
||||
entry: _Optional[_Union[SubscribeUpdateEntry, _Mapping]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeUpdateAccount(_message.Message):
|
||||
__slots__ = ("account", "slot", "is_startup")
|
||||
@@ -258,10 +420,24 @@ class SubscribeUpdateAccount(_message.Message):
|
||||
account: SubscribeUpdateAccountInfo
|
||||
slot: int
|
||||
is_startup: bool
|
||||
def __init__(self, account: _Optional[_Union[SubscribeUpdateAccountInfo, _Mapping]] = ..., slot: _Optional[int] = ..., is_startup: bool = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
account: _Optional[_Union[SubscribeUpdateAccountInfo, _Mapping]] = ...,
|
||||
slot: _Optional[int] = ...,
|
||||
is_startup: bool = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeUpdateAccountInfo(_message.Message):
|
||||
__slots__ = ("pubkey", "lamports", "owner", "executable", "rent_epoch", "data", "write_version", "txn_signature")
|
||||
__slots__ = (
|
||||
"pubkey",
|
||||
"lamports",
|
||||
"owner",
|
||||
"executable",
|
||||
"rent_epoch",
|
||||
"data",
|
||||
"write_version",
|
||||
"txn_signature",
|
||||
)
|
||||
PUBKEY_FIELD_NUMBER: _ClassVar[int]
|
||||
LAMPORTS_FIELD_NUMBER: _ClassVar[int]
|
||||
OWNER_FIELD_NUMBER: _ClassVar[int]
|
||||
@@ -278,7 +454,17 @@ class SubscribeUpdateAccountInfo(_message.Message):
|
||||
data: bytes
|
||||
write_version: int
|
||||
txn_signature: bytes
|
||||
def __init__(self, pubkey: _Optional[bytes] = ..., lamports: _Optional[int] = ..., owner: _Optional[bytes] = ..., executable: bool = ..., rent_epoch: _Optional[int] = ..., data: _Optional[bytes] = ..., write_version: _Optional[int] = ..., txn_signature: _Optional[bytes] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
pubkey: _Optional[bytes] = ...,
|
||||
lamports: _Optional[int] = ...,
|
||||
owner: _Optional[bytes] = ...,
|
||||
executable: bool = ...,
|
||||
rent_epoch: _Optional[int] = ...,
|
||||
data: _Optional[bytes] = ...,
|
||||
write_version: _Optional[int] = ...,
|
||||
txn_signature: _Optional[bytes] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeUpdateSlot(_message.Message):
|
||||
__slots__ = ("slot", "parent", "status", "dead_error")
|
||||
@@ -290,7 +476,13 @@ class SubscribeUpdateSlot(_message.Message):
|
||||
parent: int
|
||||
status: CommitmentLevel
|
||||
dead_error: str
|
||||
def __init__(self, slot: _Optional[int] = ..., parent: _Optional[int] = ..., status: _Optional[_Union[CommitmentLevel, str]] = ..., dead_error: _Optional[str] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
slot: _Optional[int] = ...,
|
||||
parent: _Optional[int] = ...,
|
||||
status: _Optional[_Union[CommitmentLevel, str]] = ...,
|
||||
dead_error: _Optional[str] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeUpdateTransaction(_message.Message):
|
||||
__slots__ = ("transaction", "slot")
|
||||
@@ -298,7 +490,11 @@ class SubscribeUpdateTransaction(_message.Message):
|
||||
SLOT_FIELD_NUMBER: _ClassVar[int]
|
||||
transaction: SubscribeUpdateTransactionInfo
|
||||
slot: int
|
||||
def __init__(self, transaction: _Optional[_Union[SubscribeUpdateTransactionInfo, _Mapping]] = ..., slot: _Optional[int] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
transaction: _Optional[_Union[SubscribeUpdateTransactionInfo, _Mapping]] = ...,
|
||||
slot: _Optional[int] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeUpdateTransactionInfo(_message.Message):
|
||||
__slots__ = ("signature", "is_vote", "transaction", "meta", "index")
|
||||
@@ -312,7 +508,16 @@ class SubscribeUpdateTransactionInfo(_message.Message):
|
||||
transaction: _solana_storage_pb2.Transaction
|
||||
meta: _solana_storage_pb2.TransactionStatusMeta
|
||||
index: int
|
||||
def __init__(self, signature: _Optional[bytes] = ..., is_vote: bool = ..., transaction: _Optional[_Union[_solana_storage_pb2.Transaction, _Mapping]] = ..., meta: _Optional[_Union[_solana_storage_pb2.TransactionStatusMeta, _Mapping]] = ..., index: _Optional[int] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
signature: _Optional[bytes] = ...,
|
||||
is_vote: bool = ...,
|
||||
transaction: _Optional[_Union[_solana_storage_pb2.Transaction, _Mapping]] = ...,
|
||||
meta: _Optional[
|
||||
_Union[_solana_storage_pb2.TransactionStatusMeta, _Mapping]
|
||||
] = ...,
|
||||
index: _Optional[int] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeUpdateTransactionStatus(_message.Message):
|
||||
__slots__ = ("slot", "signature", "is_vote", "index", "err")
|
||||
@@ -326,10 +531,31 @@ class SubscribeUpdateTransactionStatus(_message.Message):
|
||||
is_vote: bool
|
||||
index: int
|
||||
err: _solana_storage_pb2.TransactionError
|
||||
def __init__(self, slot: _Optional[int] = ..., signature: _Optional[bytes] = ..., is_vote: bool = ..., index: _Optional[int] = ..., err: _Optional[_Union[_solana_storage_pb2.TransactionError, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
slot: _Optional[int] = ...,
|
||||
signature: _Optional[bytes] = ...,
|
||||
is_vote: bool = ...,
|
||||
index: _Optional[int] = ...,
|
||||
err: _Optional[_Union[_solana_storage_pb2.TransactionError, _Mapping]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeUpdateBlock(_message.Message):
|
||||
__slots__ = ("slot", "blockhash", "rewards", "block_time", "block_height", "parent_slot", "parent_blockhash", "executed_transaction_count", "transactions", "updated_account_count", "accounts", "entries_count", "entries")
|
||||
__slots__ = (
|
||||
"slot",
|
||||
"blockhash",
|
||||
"rewards",
|
||||
"block_time",
|
||||
"block_height",
|
||||
"parent_slot",
|
||||
"parent_blockhash",
|
||||
"executed_transaction_count",
|
||||
"transactions",
|
||||
"updated_account_count",
|
||||
"accounts",
|
||||
"entries_count",
|
||||
"entries",
|
||||
)
|
||||
SLOT_FIELD_NUMBER: _ClassVar[int]
|
||||
BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
|
||||
REWARDS_FIELD_NUMBER: _ClassVar[int]
|
||||
@@ -351,15 +577,50 @@ class SubscribeUpdateBlock(_message.Message):
|
||||
parent_slot: int
|
||||
parent_blockhash: str
|
||||
executed_transaction_count: int
|
||||
transactions: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateTransactionInfo]
|
||||
transactions: _containers.RepeatedCompositeFieldContainer[
|
||||
SubscribeUpdateTransactionInfo
|
||||
]
|
||||
updated_account_count: int
|
||||
accounts: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateAccountInfo]
|
||||
entries_count: int
|
||||
entries: _containers.RepeatedCompositeFieldContainer[SubscribeUpdateEntry]
|
||||
def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., rewards: _Optional[_Union[_solana_storage_pb2.Rewards, _Mapping]] = ..., block_time: _Optional[_Union[_solana_storage_pb2.UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[_solana_storage_pb2.BlockHeight, _Mapping]] = ..., parent_slot: _Optional[int] = ..., parent_blockhash: _Optional[str] = ..., executed_transaction_count: _Optional[int] = ..., transactions: _Optional[_Iterable[_Union[SubscribeUpdateTransactionInfo, _Mapping]]] = ..., updated_account_count: _Optional[int] = ..., accounts: _Optional[_Iterable[_Union[SubscribeUpdateAccountInfo, _Mapping]]] = ..., entries_count: _Optional[int] = ..., entries: _Optional[_Iterable[_Union[SubscribeUpdateEntry, _Mapping]]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
slot: _Optional[int] = ...,
|
||||
blockhash: _Optional[str] = ...,
|
||||
rewards: _Optional[_Union[_solana_storage_pb2.Rewards, _Mapping]] = ...,
|
||||
block_time: _Optional[
|
||||
_Union[_solana_storage_pb2.UnixTimestamp, _Mapping]
|
||||
] = ...,
|
||||
block_height: _Optional[
|
||||
_Union[_solana_storage_pb2.BlockHeight, _Mapping]
|
||||
] = ...,
|
||||
parent_slot: _Optional[int] = ...,
|
||||
parent_blockhash: _Optional[str] = ...,
|
||||
executed_transaction_count: _Optional[int] = ...,
|
||||
transactions: _Optional[
|
||||
_Iterable[_Union[SubscribeUpdateTransactionInfo, _Mapping]]
|
||||
] = ...,
|
||||
updated_account_count: _Optional[int] = ...,
|
||||
accounts: _Optional[
|
||||
_Iterable[_Union[SubscribeUpdateAccountInfo, _Mapping]]
|
||||
] = ...,
|
||||
entries_count: _Optional[int] = ...,
|
||||
entries: _Optional[_Iterable[_Union[SubscribeUpdateEntry, _Mapping]]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeUpdateBlockMeta(_message.Message):
|
||||
__slots__ = ("slot", "blockhash", "rewards", "block_time", "block_height", "parent_slot", "parent_blockhash", "executed_transaction_count", "entries_count")
|
||||
__slots__ = (
|
||||
"slot",
|
||||
"blockhash",
|
||||
"rewards",
|
||||
"block_time",
|
||||
"block_height",
|
||||
"parent_slot",
|
||||
"parent_blockhash",
|
||||
"executed_transaction_count",
|
||||
"entries_count",
|
||||
)
|
||||
SLOT_FIELD_NUMBER: _ClassVar[int]
|
||||
BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
|
||||
REWARDS_FIELD_NUMBER: _ClassVar[int]
|
||||
@@ -378,10 +639,32 @@ class SubscribeUpdateBlockMeta(_message.Message):
|
||||
parent_blockhash: str
|
||||
executed_transaction_count: int
|
||||
entries_count: int
|
||||
def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., rewards: _Optional[_Union[_solana_storage_pb2.Rewards, _Mapping]] = ..., block_time: _Optional[_Union[_solana_storage_pb2.UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[_solana_storage_pb2.BlockHeight, _Mapping]] = ..., parent_slot: _Optional[int] = ..., parent_blockhash: _Optional[str] = ..., executed_transaction_count: _Optional[int] = ..., entries_count: _Optional[int] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
slot: _Optional[int] = ...,
|
||||
blockhash: _Optional[str] = ...,
|
||||
rewards: _Optional[_Union[_solana_storage_pb2.Rewards, _Mapping]] = ...,
|
||||
block_time: _Optional[
|
||||
_Union[_solana_storage_pb2.UnixTimestamp, _Mapping]
|
||||
] = ...,
|
||||
block_height: _Optional[
|
||||
_Union[_solana_storage_pb2.BlockHeight, _Mapping]
|
||||
] = ...,
|
||||
parent_slot: _Optional[int] = ...,
|
||||
parent_blockhash: _Optional[str] = ...,
|
||||
executed_transaction_count: _Optional[int] = ...,
|
||||
entries_count: _Optional[int] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeUpdateEntry(_message.Message):
|
||||
__slots__ = ("slot", "index", "num_hashes", "hash", "executed_transaction_count", "starting_transaction_index")
|
||||
__slots__ = (
|
||||
"slot",
|
||||
"index",
|
||||
"num_hashes",
|
||||
"hash",
|
||||
"executed_transaction_count",
|
||||
"starting_transaction_index",
|
||||
)
|
||||
SLOT_FIELD_NUMBER: _ClassVar[int]
|
||||
INDEX_FIELD_NUMBER: _ClassVar[int]
|
||||
NUM_HASHES_FIELD_NUMBER: _ClassVar[int]
|
||||
@@ -394,7 +677,15 @@ class SubscribeUpdateEntry(_message.Message):
|
||||
hash: bytes
|
||||
executed_transaction_count: int
|
||||
starting_transaction_index: int
|
||||
def __init__(self, slot: _Optional[int] = ..., index: _Optional[int] = ..., num_hashes: _Optional[int] = ..., hash: _Optional[bytes] = ..., executed_transaction_count: _Optional[int] = ..., starting_transaction_index: _Optional[int] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
slot: _Optional[int] = ...,
|
||||
index: _Optional[int] = ...,
|
||||
num_hashes: _Optional[int] = ...,
|
||||
hash: _Optional[bytes] = ...,
|
||||
executed_transaction_count: _Optional[int] = ...,
|
||||
starting_transaction_index: _Optional[int] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class SubscribeUpdatePing(_message.Message):
|
||||
__slots__ = ()
|
||||
@@ -422,7 +713,9 @@ class GetLatestBlockhashRequest(_message.Message):
|
||||
__slots__ = ("commitment",)
|
||||
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
|
||||
commitment: CommitmentLevel
|
||||
def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...
|
||||
) -> None: ...
|
||||
|
||||
class GetLatestBlockhashResponse(_message.Message):
|
||||
__slots__ = ("slot", "blockhash", "last_valid_block_height")
|
||||
@@ -432,13 +725,20 @@ class GetLatestBlockhashResponse(_message.Message):
|
||||
slot: int
|
||||
blockhash: str
|
||||
last_valid_block_height: int
|
||||
def __init__(self, slot: _Optional[int] = ..., blockhash: _Optional[str] = ..., last_valid_block_height: _Optional[int] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
slot: _Optional[int] = ...,
|
||||
blockhash: _Optional[str] = ...,
|
||||
last_valid_block_height: _Optional[int] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class GetBlockHeightRequest(_message.Message):
|
||||
__slots__ = ("commitment",)
|
||||
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
|
||||
commitment: CommitmentLevel
|
||||
def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...
|
||||
) -> None: ...
|
||||
|
||||
class GetBlockHeightResponse(_message.Message):
|
||||
__slots__ = ("block_height",)
|
||||
@@ -450,7 +750,9 @@ class GetSlotRequest(_message.Message):
|
||||
__slots__ = ("commitment",)
|
||||
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
|
||||
commitment: CommitmentLevel
|
||||
def __init__(self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self, commitment: _Optional[_Union[CommitmentLevel, str]] = ...
|
||||
) -> None: ...
|
||||
|
||||
class GetSlotResponse(_message.Message):
|
||||
__slots__ = ("slot",)
|
||||
@@ -474,7 +776,11 @@ class IsBlockhashValidRequest(_message.Message):
|
||||
COMMITMENT_FIELD_NUMBER: _ClassVar[int]
|
||||
blockhash: str
|
||||
commitment: CommitmentLevel
|
||||
def __init__(self, blockhash: _Optional[str] = ..., commitment: _Optional[_Union[CommitmentLevel, str]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
blockhash: _Optional[str] = ...,
|
||||
commitment: _Optional[_Union[CommitmentLevel, str]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class IsBlockhashValidResponse(_message.Message):
|
||||
__slots__ = ("slot", "valid")
|
||||
|
||||
@@ -5,23 +5,26 @@ import grpc
|
||||
|
||||
import geyser.generated.geyser_pb2 as geyser__pb2
|
||||
|
||||
GRPC_GENERATED_VERSION = '1.71.0'
|
||||
GRPC_GENERATED_VERSION = "1.71.0"
|
||||
GRPC_VERSION = grpc.__version__
|
||||
_version_not_supported = False
|
||||
|
||||
try:
|
||||
from grpc._utilities import first_version_is_lower
|
||||
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
||||
|
||||
_version_not_supported = first_version_is_lower(
|
||||
GRPC_VERSION, GRPC_GENERATED_VERSION
|
||||
)
|
||||
except ImportError:
|
||||
_version_not_supported = True
|
||||
|
||||
if _version_not_supported:
|
||||
raise RuntimeError(
|
||||
f'The grpc package installed is at version {GRPC_VERSION},'
|
||||
+ ' but the generated code in geyser_pb2_grpc.py depends on'
|
||||
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
||||
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
||||
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
||||
f"The grpc package installed is at version {GRPC_VERSION},"
|
||||
+ " but the generated code in geyser_pb2_grpc.py depends on"
|
||||
+ f" grpcio>={GRPC_GENERATED_VERSION}."
|
||||
+ f" Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}"
|
||||
+ f" or downgrade your generated code using grpcio-tools<={GRPC_VERSION}."
|
||||
)
|
||||
|
||||
|
||||
@@ -35,40 +38,47 @@ class GeyserStub:
|
||||
channel: A grpc.Channel.
|
||||
"""
|
||||
self.Subscribe = channel.stream_stream(
|
||||
'/geyser.Geyser/Subscribe',
|
||||
request_serializer=geyser__pb2.SubscribeRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.SubscribeUpdate.FromString,
|
||||
_registered_method=True)
|
||||
"/geyser.Geyser/Subscribe",
|
||||
request_serializer=geyser__pb2.SubscribeRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.SubscribeUpdate.FromString,
|
||||
_registered_method=True,
|
||||
)
|
||||
self.Ping = channel.unary_unary(
|
||||
'/geyser.Geyser/Ping',
|
||||
request_serializer=geyser__pb2.PingRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.PongResponse.FromString,
|
||||
_registered_method=True)
|
||||
"/geyser.Geyser/Ping",
|
||||
request_serializer=geyser__pb2.PingRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.PongResponse.FromString,
|
||||
_registered_method=True,
|
||||
)
|
||||
self.GetLatestBlockhash = channel.unary_unary(
|
||||
'/geyser.Geyser/GetLatestBlockhash',
|
||||
request_serializer=geyser__pb2.GetLatestBlockhashRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.GetLatestBlockhashResponse.FromString,
|
||||
_registered_method=True)
|
||||
"/geyser.Geyser/GetLatestBlockhash",
|
||||
request_serializer=geyser__pb2.GetLatestBlockhashRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.GetLatestBlockhashResponse.FromString,
|
||||
_registered_method=True,
|
||||
)
|
||||
self.GetBlockHeight = channel.unary_unary(
|
||||
'/geyser.Geyser/GetBlockHeight',
|
||||
request_serializer=geyser__pb2.GetBlockHeightRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.GetBlockHeightResponse.FromString,
|
||||
_registered_method=True)
|
||||
"/geyser.Geyser/GetBlockHeight",
|
||||
request_serializer=geyser__pb2.GetBlockHeightRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.GetBlockHeightResponse.FromString,
|
||||
_registered_method=True,
|
||||
)
|
||||
self.GetSlot = channel.unary_unary(
|
||||
'/geyser.Geyser/GetSlot',
|
||||
request_serializer=geyser__pb2.GetSlotRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.GetSlotResponse.FromString,
|
||||
_registered_method=True)
|
||||
"/geyser.Geyser/GetSlot",
|
||||
request_serializer=geyser__pb2.GetSlotRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.GetSlotResponse.FromString,
|
||||
_registered_method=True,
|
||||
)
|
||||
self.IsBlockhashValid = channel.unary_unary(
|
||||
'/geyser.Geyser/IsBlockhashValid',
|
||||
request_serializer=geyser__pb2.IsBlockhashValidRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.IsBlockhashValidResponse.FromString,
|
||||
_registered_method=True)
|
||||
"/geyser.Geyser/IsBlockhashValid",
|
||||
request_serializer=geyser__pb2.IsBlockhashValidRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.IsBlockhashValidResponse.FromString,
|
||||
_registered_method=True,
|
||||
)
|
||||
self.GetVersion = channel.unary_unary(
|
||||
'/geyser.Geyser/GetVersion',
|
||||
request_serializer=geyser__pb2.GetVersionRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.GetVersionResponse.FromString,
|
||||
_registered_method=True)
|
||||
"/geyser.Geyser/GetVersion",
|
||||
request_serializer=geyser__pb2.GetVersionRequest.SerializeToString,
|
||||
response_deserializer=geyser__pb2.GetVersionResponse.FromString,
|
||||
_registered_method=True,
|
||||
)
|
||||
|
||||
|
||||
class GeyserServicer:
|
||||
@@ -77,109 +87,112 @@ class GeyserServicer:
|
||||
def Subscribe(self, request_iterator, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
context.set_details("Method not implemented!")
|
||||
raise NotImplementedError("Method not implemented!")
|
||||
|
||||
def Ping(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
context.set_details("Method not implemented!")
|
||||
raise NotImplementedError("Method not implemented!")
|
||||
|
||||
def GetLatestBlockhash(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
context.set_details("Method not implemented!")
|
||||
raise NotImplementedError("Method not implemented!")
|
||||
|
||||
def GetBlockHeight(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
context.set_details("Method not implemented!")
|
||||
raise NotImplementedError("Method not implemented!")
|
||||
|
||||
def GetSlot(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
context.set_details("Method not implemented!")
|
||||
raise NotImplementedError("Method not implemented!")
|
||||
|
||||
def IsBlockhashValid(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
context.set_details("Method not implemented!")
|
||||
raise NotImplementedError("Method not implemented!")
|
||||
|
||||
def GetVersion(self, request, context):
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
|
||||
context.set_details('Method not implemented!')
|
||||
raise NotImplementedError('Method not implemented!')
|
||||
context.set_details("Method not implemented!")
|
||||
raise NotImplementedError("Method not implemented!")
|
||||
|
||||
|
||||
def add_GeyserServicer_to_server(servicer, server):
|
||||
rpc_method_handlers = {
|
||||
'Subscribe': grpc.stream_stream_rpc_method_handler(
|
||||
servicer.Subscribe,
|
||||
request_deserializer=geyser__pb2.SubscribeRequest.FromString,
|
||||
response_serializer=geyser__pb2.SubscribeUpdate.SerializeToString,
|
||||
),
|
||||
'Ping': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.Ping,
|
||||
request_deserializer=geyser__pb2.PingRequest.FromString,
|
||||
response_serializer=geyser__pb2.PongResponse.SerializeToString,
|
||||
),
|
||||
'GetLatestBlockhash': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetLatestBlockhash,
|
||||
request_deserializer=geyser__pb2.GetLatestBlockhashRequest.FromString,
|
||||
response_serializer=geyser__pb2.GetLatestBlockhashResponse.SerializeToString,
|
||||
),
|
||||
'GetBlockHeight': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetBlockHeight,
|
||||
request_deserializer=geyser__pb2.GetBlockHeightRequest.FromString,
|
||||
response_serializer=geyser__pb2.GetBlockHeightResponse.SerializeToString,
|
||||
),
|
||||
'GetSlot': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetSlot,
|
||||
request_deserializer=geyser__pb2.GetSlotRequest.FromString,
|
||||
response_serializer=geyser__pb2.GetSlotResponse.SerializeToString,
|
||||
),
|
||||
'IsBlockhashValid': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.IsBlockhashValid,
|
||||
request_deserializer=geyser__pb2.IsBlockhashValidRequest.FromString,
|
||||
response_serializer=geyser__pb2.IsBlockhashValidResponse.SerializeToString,
|
||||
),
|
||||
'GetVersion': grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetVersion,
|
||||
request_deserializer=geyser__pb2.GetVersionRequest.FromString,
|
||||
response_serializer=geyser__pb2.GetVersionResponse.SerializeToString,
|
||||
),
|
||||
"Subscribe": grpc.stream_stream_rpc_method_handler(
|
||||
servicer.Subscribe,
|
||||
request_deserializer=geyser__pb2.SubscribeRequest.FromString,
|
||||
response_serializer=geyser__pb2.SubscribeUpdate.SerializeToString,
|
||||
),
|
||||
"Ping": grpc.unary_unary_rpc_method_handler(
|
||||
servicer.Ping,
|
||||
request_deserializer=geyser__pb2.PingRequest.FromString,
|
||||
response_serializer=geyser__pb2.PongResponse.SerializeToString,
|
||||
),
|
||||
"GetLatestBlockhash": grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetLatestBlockhash,
|
||||
request_deserializer=geyser__pb2.GetLatestBlockhashRequest.FromString,
|
||||
response_serializer=geyser__pb2.GetLatestBlockhashResponse.SerializeToString,
|
||||
),
|
||||
"GetBlockHeight": grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetBlockHeight,
|
||||
request_deserializer=geyser__pb2.GetBlockHeightRequest.FromString,
|
||||
response_serializer=geyser__pb2.GetBlockHeightResponse.SerializeToString,
|
||||
),
|
||||
"GetSlot": grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetSlot,
|
||||
request_deserializer=geyser__pb2.GetSlotRequest.FromString,
|
||||
response_serializer=geyser__pb2.GetSlotResponse.SerializeToString,
|
||||
),
|
||||
"IsBlockhashValid": grpc.unary_unary_rpc_method_handler(
|
||||
servicer.IsBlockhashValid,
|
||||
request_deserializer=geyser__pb2.IsBlockhashValidRequest.FromString,
|
||||
response_serializer=geyser__pb2.IsBlockhashValidResponse.SerializeToString,
|
||||
),
|
||||
"GetVersion": grpc.unary_unary_rpc_method_handler(
|
||||
servicer.GetVersion,
|
||||
request_deserializer=geyser__pb2.GetVersionRequest.FromString,
|
||||
response_serializer=geyser__pb2.GetVersionResponse.SerializeToString,
|
||||
),
|
||||
}
|
||||
generic_handler = grpc.method_handlers_generic_handler(
|
||||
'geyser.Geyser', rpc_method_handlers)
|
||||
"geyser.Geyser", rpc_method_handlers
|
||||
)
|
||||
server.add_generic_rpc_handlers((generic_handler,))
|
||||
server.add_registered_method_handlers('geyser.Geyser', rpc_method_handlers)
|
||||
server.add_registered_method_handlers("geyser.Geyser", rpc_method_handlers)
|
||||
|
||||
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
# This class is part of an EXPERIMENTAL API.
|
||||
class Geyser:
|
||||
"""Missing associated documentation comment in .proto file."""
|
||||
|
||||
@staticmethod
|
||||
def Subscribe(request_iterator,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
def Subscribe(
|
||||
request_iterator,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None,
|
||||
):
|
||||
return grpc.experimental.stream_stream(
|
||||
request_iterator,
|
||||
target,
|
||||
'/geyser.Geyser/Subscribe',
|
||||
"/geyser.Geyser/Subscribe",
|
||||
geyser__pb2.SubscribeRequest.SerializeToString,
|
||||
geyser__pb2.SubscribeUpdate.FromString,
|
||||
options,
|
||||
@@ -190,23 +203,26 @@ class Geyser:
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
_registered_method=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def Ping(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
def Ping(
|
||||
request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None,
|
||||
):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/geyser.Geyser/Ping',
|
||||
"/geyser.Geyser/Ping",
|
||||
geyser__pb2.PingRequest.SerializeToString,
|
||||
geyser__pb2.PongResponse.FromString,
|
||||
options,
|
||||
@@ -217,23 +233,26 @@ class Geyser:
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
_registered_method=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def GetLatestBlockhash(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
def GetLatestBlockhash(
|
||||
request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None,
|
||||
):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/geyser.Geyser/GetLatestBlockhash',
|
||||
"/geyser.Geyser/GetLatestBlockhash",
|
||||
geyser__pb2.GetLatestBlockhashRequest.SerializeToString,
|
||||
geyser__pb2.GetLatestBlockhashResponse.FromString,
|
||||
options,
|
||||
@@ -244,23 +263,26 @@ class Geyser:
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
_registered_method=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def GetBlockHeight(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
def GetBlockHeight(
|
||||
request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None,
|
||||
):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/geyser.Geyser/GetBlockHeight',
|
||||
"/geyser.Geyser/GetBlockHeight",
|
||||
geyser__pb2.GetBlockHeightRequest.SerializeToString,
|
||||
geyser__pb2.GetBlockHeightResponse.FromString,
|
||||
options,
|
||||
@@ -271,23 +293,26 @@ class Geyser:
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
_registered_method=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def GetSlot(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
def GetSlot(
|
||||
request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None,
|
||||
):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/geyser.Geyser/GetSlot',
|
||||
"/geyser.Geyser/GetSlot",
|
||||
geyser__pb2.GetSlotRequest.SerializeToString,
|
||||
geyser__pb2.GetSlotResponse.FromString,
|
||||
options,
|
||||
@@ -298,23 +323,26 @@ class Geyser:
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
_registered_method=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def IsBlockhashValid(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
def IsBlockhashValid(
|
||||
request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None,
|
||||
):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/geyser.Geyser/IsBlockhashValid',
|
||||
"/geyser.Geyser/IsBlockhashValid",
|
||||
geyser__pb2.IsBlockhashValidRequest.SerializeToString,
|
||||
geyser__pb2.IsBlockhashValidResponse.FromString,
|
||||
options,
|
||||
@@ -325,23 +353,26 @@ class Geyser:
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
_registered_method=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def GetVersion(request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None):
|
||||
def GetVersion(
|
||||
request,
|
||||
target,
|
||||
options=(),
|
||||
channel_credentials=None,
|
||||
call_credentials=None,
|
||||
insecure=False,
|
||||
compression=None,
|
||||
wait_for_ready=None,
|
||||
timeout=None,
|
||||
metadata=None,
|
||||
):
|
||||
return grpc.experimental.unary_unary(
|
||||
request,
|
||||
target,
|
||||
'/geyser.Geyser/GetVersion',
|
||||
"/geyser.Geyser/GetVersion",
|
||||
geyser__pb2.GetVersionRequest.SerializeToString,
|
||||
geyser__pb2.GetVersionResponse.FromString,
|
||||
options,
|
||||
@@ -352,4 +383,5 @@ class Geyser:
|
||||
wait_for_ready,
|
||||
timeout,
|
||||
metadata,
|
||||
_registered_method=True)
|
||||
_registered_method=True,
|
||||
)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,13 @@ from google.protobuf.internal import containers as _containers
|
||||
from google.protobuf.internal import enum_type_wrapper as _enum_type_wrapper
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
|
||||
from typing import (
|
||||
ClassVar as _ClassVar,
|
||||
Iterable as _Iterable,
|
||||
Mapping as _Mapping,
|
||||
Optional as _Optional,
|
||||
Union as _Union,
|
||||
)
|
||||
|
||||
DESCRIPTOR: _descriptor.FileDescriptor
|
||||
|
||||
@@ -13,6 +19,7 @@ class RewardType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
|
||||
Rent: _ClassVar[RewardType]
|
||||
Staking: _ClassVar[RewardType]
|
||||
Voting: _ClassVar[RewardType]
|
||||
|
||||
Unspecified: RewardType
|
||||
Fee: RewardType
|
||||
Rent: RewardType
|
||||
@@ -20,7 +27,16 @@ Staking: RewardType
|
||||
Voting: RewardType
|
||||
|
||||
class ConfirmedBlock(_message.Message):
|
||||
__slots__ = ("previous_blockhash", "blockhash", "parent_slot", "transactions", "rewards", "block_time", "block_height", "num_partitions")
|
||||
__slots__ = (
|
||||
"previous_blockhash",
|
||||
"blockhash",
|
||||
"parent_slot",
|
||||
"transactions",
|
||||
"rewards",
|
||||
"block_time",
|
||||
"block_height",
|
||||
"num_partitions",
|
||||
)
|
||||
PREVIOUS_BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
|
||||
BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
|
||||
PARENT_SLOT_FIELD_NUMBER: _ClassVar[int]
|
||||
@@ -37,7 +53,19 @@ class ConfirmedBlock(_message.Message):
|
||||
block_time: UnixTimestamp
|
||||
block_height: BlockHeight
|
||||
num_partitions: NumPartitions
|
||||
def __init__(self, previous_blockhash: _Optional[str] = ..., blockhash: _Optional[str] = ..., parent_slot: _Optional[int] = ..., transactions: _Optional[_Iterable[_Union[ConfirmedTransaction, _Mapping]]] = ..., rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., block_time: _Optional[_Union[UnixTimestamp, _Mapping]] = ..., block_height: _Optional[_Union[BlockHeight, _Mapping]] = ..., num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
previous_blockhash: _Optional[str] = ...,
|
||||
blockhash: _Optional[str] = ...,
|
||||
parent_slot: _Optional[int] = ...,
|
||||
transactions: _Optional[
|
||||
_Iterable[_Union[ConfirmedTransaction, _Mapping]]
|
||||
] = ...,
|
||||
rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ...,
|
||||
block_time: _Optional[_Union[UnixTimestamp, _Mapping]] = ...,
|
||||
block_height: _Optional[_Union[BlockHeight, _Mapping]] = ...,
|
||||
num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class ConfirmedTransaction(_message.Message):
|
||||
__slots__ = ("transaction", "meta")
|
||||
@@ -45,7 +73,11 @@ class ConfirmedTransaction(_message.Message):
|
||||
META_FIELD_NUMBER: _ClassVar[int]
|
||||
transaction: Transaction
|
||||
meta: TransactionStatusMeta
|
||||
def __init__(self, transaction: _Optional[_Union[Transaction, _Mapping]] = ..., meta: _Optional[_Union[TransactionStatusMeta, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
transaction: _Optional[_Union[Transaction, _Mapping]] = ...,
|
||||
meta: _Optional[_Union[TransactionStatusMeta, _Mapping]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class Transaction(_message.Message):
|
||||
__slots__ = ("signatures", "message")
|
||||
@@ -53,10 +85,21 @@ class Transaction(_message.Message):
|
||||
MESSAGE_FIELD_NUMBER: _ClassVar[int]
|
||||
signatures: _containers.RepeatedScalarFieldContainer[bytes]
|
||||
message: Message
|
||||
def __init__(self, signatures: _Optional[_Iterable[bytes]] = ..., message: _Optional[_Union[Message, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
signatures: _Optional[_Iterable[bytes]] = ...,
|
||||
message: _Optional[_Union[Message, _Mapping]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class Message(_message.Message):
|
||||
__slots__ = ("header", "account_keys", "recent_blockhash", "instructions", "versioned", "address_table_lookups")
|
||||
__slots__ = (
|
||||
"header",
|
||||
"account_keys",
|
||||
"recent_blockhash",
|
||||
"instructions",
|
||||
"versioned",
|
||||
"address_table_lookups",
|
||||
)
|
||||
HEADER_FIELD_NUMBER: _ClassVar[int]
|
||||
ACCOUNT_KEYS_FIELD_NUMBER: _ClassVar[int]
|
||||
RECENT_BLOCKHASH_FIELD_NUMBER: _ClassVar[int]
|
||||
@@ -68,18 +111,39 @@ class Message(_message.Message):
|
||||
recent_blockhash: bytes
|
||||
instructions: _containers.RepeatedCompositeFieldContainer[CompiledInstruction]
|
||||
versioned: bool
|
||||
address_table_lookups: _containers.RepeatedCompositeFieldContainer[MessageAddressTableLookup]
|
||||
def __init__(self, header: _Optional[_Union[MessageHeader, _Mapping]] = ..., account_keys: _Optional[_Iterable[bytes]] = ..., recent_blockhash: _Optional[bytes] = ..., instructions: _Optional[_Iterable[_Union[CompiledInstruction, _Mapping]]] = ..., versioned: bool = ..., address_table_lookups: _Optional[_Iterable[_Union[MessageAddressTableLookup, _Mapping]]] = ...) -> None: ...
|
||||
address_table_lookups: _containers.RepeatedCompositeFieldContainer[
|
||||
MessageAddressTableLookup
|
||||
]
|
||||
def __init__(
|
||||
self,
|
||||
header: _Optional[_Union[MessageHeader, _Mapping]] = ...,
|
||||
account_keys: _Optional[_Iterable[bytes]] = ...,
|
||||
recent_blockhash: _Optional[bytes] = ...,
|
||||
instructions: _Optional[_Iterable[_Union[CompiledInstruction, _Mapping]]] = ...,
|
||||
versioned: bool = ...,
|
||||
address_table_lookups: _Optional[
|
||||
_Iterable[_Union[MessageAddressTableLookup, _Mapping]]
|
||||
] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class MessageHeader(_message.Message):
|
||||
__slots__ = ("num_required_signatures", "num_readonly_signed_accounts", "num_readonly_unsigned_accounts")
|
||||
__slots__ = (
|
||||
"num_required_signatures",
|
||||
"num_readonly_signed_accounts",
|
||||
"num_readonly_unsigned_accounts",
|
||||
)
|
||||
NUM_REQUIRED_SIGNATURES_FIELD_NUMBER: _ClassVar[int]
|
||||
NUM_READONLY_SIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
|
||||
NUM_READONLY_UNSIGNED_ACCOUNTS_FIELD_NUMBER: _ClassVar[int]
|
||||
num_required_signatures: int
|
||||
num_readonly_signed_accounts: int
|
||||
num_readonly_unsigned_accounts: int
|
||||
def __init__(self, num_required_signatures: _Optional[int] = ..., num_readonly_signed_accounts: _Optional[int] = ..., num_readonly_unsigned_accounts: _Optional[int] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
num_required_signatures: _Optional[int] = ...,
|
||||
num_readonly_signed_accounts: _Optional[int] = ...,
|
||||
num_readonly_unsigned_accounts: _Optional[int] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class MessageAddressTableLookup(_message.Message):
|
||||
__slots__ = ("account_key", "writable_indexes", "readonly_indexes")
|
||||
@@ -89,10 +153,32 @@ class MessageAddressTableLookup(_message.Message):
|
||||
account_key: bytes
|
||||
writable_indexes: bytes
|
||||
readonly_indexes: bytes
|
||||
def __init__(self, account_key: _Optional[bytes] = ..., writable_indexes: _Optional[bytes] = ..., readonly_indexes: _Optional[bytes] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
account_key: _Optional[bytes] = ...,
|
||||
writable_indexes: _Optional[bytes] = ...,
|
||||
readonly_indexes: _Optional[bytes] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class TransactionStatusMeta(_message.Message):
|
||||
__slots__ = ("err", "fee", "pre_balances", "post_balances", "inner_instructions", "inner_instructions_none", "log_messages", "log_messages_none", "pre_token_balances", "post_token_balances", "rewards", "loaded_writable_addresses", "loaded_readonly_addresses", "return_data", "return_data_none", "compute_units_consumed")
|
||||
__slots__ = (
|
||||
"err",
|
||||
"fee",
|
||||
"pre_balances",
|
||||
"post_balances",
|
||||
"inner_instructions",
|
||||
"inner_instructions_none",
|
||||
"log_messages",
|
||||
"log_messages_none",
|
||||
"pre_token_balances",
|
||||
"post_token_balances",
|
||||
"rewards",
|
||||
"loaded_writable_addresses",
|
||||
"loaded_readonly_addresses",
|
||||
"return_data",
|
||||
"return_data_none",
|
||||
"compute_units_consumed",
|
||||
)
|
||||
ERR_FIELD_NUMBER: _ClassVar[int]
|
||||
FEE_FIELD_NUMBER: _ClassVar[int]
|
||||
PRE_BALANCES_FIELD_NUMBER: _ClassVar[int]
|
||||
@@ -125,7 +211,27 @@ class TransactionStatusMeta(_message.Message):
|
||||
return_data: ReturnData
|
||||
return_data_none: bool
|
||||
compute_units_consumed: int
|
||||
def __init__(self, err: _Optional[_Union[TransactionError, _Mapping]] = ..., fee: _Optional[int] = ..., pre_balances: _Optional[_Iterable[int]] = ..., post_balances: _Optional[_Iterable[int]] = ..., inner_instructions: _Optional[_Iterable[_Union[InnerInstructions, _Mapping]]] = ..., inner_instructions_none: bool = ..., log_messages: _Optional[_Iterable[str]] = ..., log_messages_none: bool = ..., pre_token_balances: _Optional[_Iterable[_Union[TokenBalance, _Mapping]]] = ..., post_token_balances: _Optional[_Iterable[_Union[TokenBalance, _Mapping]]] = ..., rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., loaded_writable_addresses: _Optional[_Iterable[bytes]] = ..., loaded_readonly_addresses: _Optional[_Iterable[bytes]] = ..., return_data: _Optional[_Union[ReturnData, _Mapping]] = ..., return_data_none: bool = ..., compute_units_consumed: _Optional[int] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
err: _Optional[_Union[TransactionError, _Mapping]] = ...,
|
||||
fee: _Optional[int] = ...,
|
||||
pre_balances: _Optional[_Iterable[int]] = ...,
|
||||
post_balances: _Optional[_Iterable[int]] = ...,
|
||||
inner_instructions: _Optional[
|
||||
_Iterable[_Union[InnerInstructions, _Mapping]]
|
||||
] = ...,
|
||||
inner_instructions_none: bool = ...,
|
||||
log_messages: _Optional[_Iterable[str]] = ...,
|
||||
log_messages_none: bool = ...,
|
||||
pre_token_balances: _Optional[_Iterable[_Union[TokenBalance, _Mapping]]] = ...,
|
||||
post_token_balances: _Optional[_Iterable[_Union[TokenBalance, _Mapping]]] = ...,
|
||||
rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ...,
|
||||
loaded_writable_addresses: _Optional[_Iterable[bytes]] = ...,
|
||||
loaded_readonly_addresses: _Optional[_Iterable[bytes]] = ...,
|
||||
return_data: _Optional[_Union[ReturnData, _Mapping]] = ...,
|
||||
return_data_none: bool = ...,
|
||||
compute_units_consumed: _Optional[int] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class TransactionError(_message.Message):
|
||||
__slots__ = ("err",)
|
||||
@@ -139,7 +245,11 @@ class InnerInstructions(_message.Message):
|
||||
INSTRUCTIONS_FIELD_NUMBER: _ClassVar[int]
|
||||
index: int
|
||||
instructions: _containers.RepeatedCompositeFieldContainer[InnerInstruction]
|
||||
def __init__(self, index: _Optional[int] = ..., instructions: _Optional[_Iterable[_Union[InnerInstruction, _Mapping]]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
index: _Optional[int] = ...,
|
||||
instructions: _Optional[_Iterable[_Union[InnerInstruction, _Mapping]]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class InnerInstruction(_message.Message):
|
||||
__slots__ = ("program_id_index", "accounts", "data", "stack_height")
|
||||
@@ -151,7 +261,13 @@ class InnerInstruction(_message.Message):
|
||||
accounts: bytes
|
||||
data: bytes
|
||||
stack_height: int
|
||||
def __init__(self, program_id_index: _Optional[int] = ..., accounts: _Optional[bytes] = ..., data: _Optional[bytes] = ..., stack_height: _Optional[int] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
program_id_index: _Optional[int] = ...,
|
||||
accounts: _Optional[bytes] = ...,
|
||||
data: _Optional[bytes] = ...,
|
||||
stack_height: _Optional[int] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class CompiledInstruction(_message.Message):
|
||||
__slots__ = ("program_id_index", "accounts", "data")
|
||||
@@ -161,7 +277,12 @@ class CompiledInstruction(_message.Message):
|
||||
program_id_index: int
|
||||
accounts: bytes
|
||||
data: bytes
|
||||
def __init__(self, program_id_index: _Optional[int] = ..., accounts: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
program_id_index: _Optional[int] = ...,
|
||||
accounts: _Optional[bytes] = ...,
|
||||
data: _Optional[bytes] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class TokenBalance(_message.Message):
|
||||
__slots__ = ("account_index", "mint", "ui_token_amount", "owner", "program_id")
|
||||
@@ -175,7 +296,14 @@ class TokenBalance(_message.Message):
|
||||
ui_token_amount: UiTokenAmount
|
||||
owner: str
|
||||
program_id: str
|
||||
def __init__(self, account_index: _Optional[int] = ..., mint: _Optional[str] = ..., ui_token_amount: _Optional[_Union[UiTokenAmount, _Mapping]] = ..., owner: _Optional[str] = ..., program_id: _Optional[str] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
account_index: _Optional[int] = ...,
|
||||
mint: _Optional[str] = ...,
|
||||
ui_token_amount: _Optional[_Union[UiTokenAmount, _Mapping]] = ...,
|
||||
owner: _Optional[str] = ...,
|
||||
program_id: _Optional[str] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class UiTokenAmount(_message.Message):
|
||||
__slots__ = ("ui_amount", "decimals", "amount", "ui_amount_string")
|
||||
@@ -187,7 +315,13 @@ class UiTokenAmount(_message.Message):
|
||||
decimals: int
|
||||
amount: str
|
||||
ui_amount_string: str
|
||||
def __init__(self, ui_amount: _Optional[float] = ..., decimals: _Optional[int] = ..., amount: _Optional[str] = ..., ui_amount_string: _Optional[str] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
ui_amount: _Optional[float] = ...,
|
||||
decimals: _Optional[int] = ...,
|
||||
amount: _Optional[str] = ...,
|
||||
ui_amount_string: _Optional[str] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class ReturnData(_message.Message):
|
||||
__slots__ = ("program_id", "data")
|
||||
@@ -195,7 +329,9 @@ class ReturnData(_message.Message):
|
||||
DATA_FIELD_NUMBER: _ClassVar[int]
|
||||
program_id: bytes
|
||||
data: bytes
|
||||
def __init__(self, program_id: _Optional[bytes] = ..., data: _Optional[bytes] = ...) -> None: ...
|
||||
def __init__(
|
||||
self, program_id: _Optional[bytes] = ..., data: _Optional[bytes] = ...
|
||||
) -> None: ...
|
||||
|
||||
class Reward(_message.Message):
|
||||
__slots__ = ("pubkey", "lamports", "post_balance", "reward_type", "commission")
|
||||
@@ -209,7 +345,14 @@ class Reward(_message.Message):
|
||||
post_balance: int
|
||||
reward_type: RewardType
|
||||
commission: str
|
||||
def __init__(self, pubkey: _Optional[str] = ..., lamports: _Optional[int] = ..., post_balance: _Optional[int] = ..., reward_type: _Optional[_Union[RewardType, str]] = ..., commission: _Optional[str] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
pubkey: _Optional[str] = ...,
|
||||
lamports: _Optional[int] = ...,
|
||||
post_balance: _Optional[int] = ...,
|
||||
reward_type: _Optional[_Union[RewardType, str]] = ...,
|
||||
commission: _Optional[str] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class Rewards(_message.Message):
|
||||
__slots__ = ("rewards", "num_partitions")
|
||||
@@ -217,7 +360,11 @@ class Rewards(_message.Message):
|
||||
NUM_PARTITIONS_FIELD_NUMBER: _ClassVar[int]
|
||||
rewards: _containers.RepeatedCompositeFieldContainer[Reward]
|
||||
num_partitions: NumPartitions
|
||||
def __init__(self, rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ..., num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...) -> None: ...
|
||||
def __init__(
|
||||
self,
|
||||
rewards: _Optional[_Iterable[_Union[Reward, _Mapping]]] = ...,
|
||||
num_partitions: _Optional[_Union[NumPartitions, _Mapping]] = ...,
|
||||
) -> None: ...
|
||||
|
||||
class UnixTimestamp(_message.Message):
|
||||
__slots__ = ("timestamp",)
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
|
||||
"""Client and server classes corresponding to protobuf-defined services."""
|
||||
|
||||
import grpc
|
||||
import warnings
|
||||
|
||||
|
||||
GRPC_GENERATED_VERSION = '1.71.0'
|
||||
GRPC_GENERATED_VERSION = "1.71.0"
|
||||
GRPC_VERSION = grpc.__version__
|
||||
_version_not_supported = False
|
||||
|
||||
try:
|
||||
from grpc._utilities import first_version_is_lower
|
||||
_version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
|
||||
|
||||
_version_not_supported = first_version_is_lower(
|
||||
GRPC_VERSION, GRPC_GENERATED_VERSION
|
||||
)
|
||||
except ImportError:
|
||||
_version_not_supported = True
|
||||
|
||||
if _version_not_supported:
|
||||
raise RuntimeError(
|
||||
f'The grpc package installed is at version {GRPC_VERSION},'
|
||||
+ f' but the generated code in solana_storage_pb2_grpc.py depends on'
|
||||
+ f' grpcio>={GRPC_GENERATED_VERSION}.'
|
||||
+ f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
|
||||
+ f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
|
||||
f"The grpc package installed is at version {GRPC_VERSION},"
|
||||
+ f" but the generated code in solana_storage_pb2_grpc.py depends on"
|
||||
+ f" grpcio>={GRPC_GENERATED_VERSION}."
|
||||
+ f" Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}"
|
||||
+ f" or downgrade your generated code using grpcio-tools<={GRPC_VERSION}."
|
||||
)
|
||||
|
||||
@@ -172,7 +172,7 @@ class PumpEventProcessor:
|
||||
args["user"] = str(accounts[7])
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def _find_creator_vault(self, creator: Pubkey) -> Pubkey:
|
||||
"""
|
||||
Find the creator vault for a creator.
|
||||
@@ -184,10 +184,7 @@ class PumpEventProcessor:
|
||||
Creator vault address
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
b"creator-vault",
|
||||
bytes(creator)
|
||||
],
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpAddresses.PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
|
||||
@@ -105,7 +105,7 @@ class BlockListener(BaseTokenListener):
|
||||
{"mentionsAccountOrProgram": str(self.pump_program)},
|
||||
{
|
||||
"commitment": "confirmed",
|
||||
"encoding": "base64", # base64 is faster than other encoding options
|
||||
"encoding": "base64", # base64 is faster than other encoding options
|
||||
"showRewards": False,
|
||||
"transactionDetails": "full",
|
||||
"maxSupportedTransactionVersion": 0,
|
||||
|
||||
@@ -28,7 +28,9 @@ class GeyserEventProcessor:
|
||||
"""
|
||||
self.pump_program = pump_program
|
||||
|
||||
def process_transaction_data(self, instruction_data: bytes, accounts: list, keys: list) -> TokenInfo | None:
|
||||
def process_transaction_data(
|
||||
self, instruction_data: bytes, accounts: list, keys: list
|
||||
) -> TokenInfo | None:
|
||||
"""Process transaction data and extract token creation info.
|
||||
|
||||
Args:
|
||||
@@ -45,7 +47,7 @@ class GeyserEventProcessor:
|
||||
try:
|
||||
# Skip past the 8-byte discriminator
|
||||
offset = 8
|
||||
|
||||
|
||||
# Helper to read strings (prefixed with length)
|
||||
def read_string():
|
||||
nonlocal offset
|
||||
@@ -53,16 +55,18 @@ class GeyserEventProcessor:
|
||||
length = struct.unpack_from("<I", instruction_data, offset)[0]
|
||||
offset += 4
|
||||
# Extract and decode the string
|
||||
value = instruction_data[offset:offset + length].decode("utf-8")
|
||||
value = instruction_data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
return value
|
||||
|
||||
|
||||
def read_pubkey():
|
||||
nonlocal offset
|
||||
value = base58.b58encode(instruction_data[offset : offset + 32]).decode("utf-8")
|
||||
value = base58.b58encode(instruction_data[offset : offset + 32]).decode(
|
||||
"utf-8"
|
||||
)
|
||||
offset += 32
|
||||
return Pubkey.from_string(value)
|
||||
|
||||
|
||||
# Helper to get account key
|
||||
def get_account_key(index):
|
||||
if index >= len(accounts):
|
||||
@@ -71,7 +75,7 @@ class GeyserEventProcessor:
|
||||
if account_index >= len(keys):
|
||||
return None
|
||||
return Pubkey.from_bytes(keys[account_index])
|
||||
|
||||
|
||||
name = read_string()
|
||||
symbol = read_string()
|
||||
uri = read_string()
|
||||
@@ -83,11 +87,11 @@ class GeyserEventProcessor:
|
||||
user = get_account_key(7)
|
||||
|
||||
creator_vault = self._find_creator_vault(creator)
|
||||
|
||||
|
||||
if not all([mint, bonding_curve, associated_bonding_curve, user]):
|
||||
logger.warning("Missing required account keys in token creation")
|
||||
return None
|
||||
|
||||
|
||||
return TokenInfo(
|
||||
name=name,
|
||||
symbol=symbol,
|
||||
@@ -99,11 +103,11 @@ class GeyserEventProcessor:
|
||||
creator=creator,
|
||||
creator_vault=creator_vault,
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process transaction data: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _find_creator_vault(self, creator: Pubkey) -> Pubkey:
|
||||
"""
|
||||
Find the creator vault for a creator.
|
||||
@@ -115,10 +119,7 @@ class GeyserEventProcessor:
|
||||
Creator vault address
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
b"creator-vault",
|
||||
bytes(creator)
|
||||
],
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpAddresses.PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
return derived_address
|
||||
|
||||
@@ -20,9 +20,15 @@ logger = get_logger(__name__)
|
||||
class GeyserListener(BaseTokenListener):
|
||||
"""Geyser listener for pump.fun token creation events."""
|
||||
|
||||
def __init__(self, geyser_endpoint: str, geyser_api_token: str, geyser_auth_type: str, pump_program: Pubkey):
|
||||
def __init__(
|
||||
self,
|
||||
geyser_endpoint: str,
|
||||
geyser_api_token: str,
|
||||
geyser_auth_type: str,
|
||||
pump_program: Pubkey,
|
||||
):
|
||||
"""Initialize token listener.
|
||||
|
||||
|
||||
Args:
|
||||
geyser_endpoint: Geyser gRPC endpoint URL
|
||||
geyser_api_token: API token for authentication
|
||||
@@ -40,27 +46,31 @@ class GeyserListener(BaseTokenListener):
|
||||
)
|
||||
self.pump_program = pump_program
|
||||
self.event_processor = GeyserEventProcessor(pump_program)
|
||||
|
||||
|
||||
async def _create_geyser_connection(self):
|
||||
"""Establish a secure connection to the Geyser endpoint."""
|
||||
if self.auth_type == "x-token":
|
||||
auth = grpc.metadata_call_credentials(
|
||||
lambda _, callback: callback((("x-token", self.geyser_api_token),), None)
|
||||
lambda _, callback: callback(
|
||||
(("x-token", self.geyser_api_token),), None
|
||||
)
|
||||
)
|
||||
else: # Default to basic auth
|
||||
auth = grpc.metadata_call_credentials(
|
||||
lambda _, callback: callback((("authorization", f"Basic {self.geyser_api_token}"),), None)
|
||||
lambda _, callback: callback(
|
||||
(("authorization", f"Basic {self.geyser_api_token}"),), None
|
||||
)
|
||||
)
|
||||
creds = grpc.composite_channel_credentials(
|
||||
grpc.ssl_channel_credentials(), auth
|
||||
)
|
||||
creds = grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth)
|
||||
channel = grpc.aio.secure_channel(self.geyser_endpoint, creds)
|
||||
return geyser_pb2_grpc.GeyserStub(channel), channel
|
||||
|
||||
def _create_subscription_request(self):
|
||||
"""Create a subscription request for Pump.fun transactions."""
|
||||
request = geyser_pb2.SubscribeRequest()
|
||||
request.transactions["pump_filter"].account_include.append(str(self.pump_program))
|
||||
request.transactions["pump_filter"].account_include.append(
|
||||
str(self.pump_program)
|
||||
)
|
||||
request.transactions["pump_filter"].failed = False
|
||||
request.commitment = geyser_pb2.CommitmentLevel.PROCESSED
|
||||
return request
|
||||
@@ -72,7 +82,7 @@ class GeyserListener(BaseTokenListener):
|
||||
creator_address: str | None = None,
|
||||
) -> None:
|
||||
"""Listen for new token creations using Geyser subscription.
|
||||
|
||||
|
||||
Args:
|
||||
token_callback: Callback function for new tokens
|
||||
match_string: Optional string to match in token name/symbol
|
||||
@@ -82,20 +92,22 @@ class GeyserListener(BaseTokenListener):
|
||||
try:
|
||||
stub, channel = await self._create_geyser_connection()
|
||||
request = self._create_subscription_request()
|
||||
|
||||
|
||||
logger.info(f"Connected to Geyser endpoint: {self.geyser_endpoint}")
|
||||
logger.info(f"Monitoring for transactions involving program: {self.pump_program}")
|
||||
|
||||
logger.info(
|
||||
f"Monitoring for transactions involving program: {self.pump_program}"
|
||||
)
|
||||
|
||||
try:
|
||||
async for update in stub.Subscribe(iter([request])):
|
||||
token_info = await self._process_update(update)
|
||||
if not token_info:
|
||||
continue
|
||||
|
||||
|
||||
logger.info(
|
||||
f"New token detected: {token_info.name} ({token_info.symbol})"
|
||||
)
|
||||
|
||||
|
||||
if match_string and not (
|
||||
match_string.lower() in token_info.name.lower()
|
||||
or match_string.lower() in token_info.symbol.lower()
|
||||
@@ -104,43 +116,40 @@ class GeyserListener(BaseTokenListener):
|
||||
f"Token does not match filter '{match_string}'. Skipping..."
|
||||
)
|
||||
continue
|
||||
|
||||
if (
|
||||
creator_address
|
||||
and str(token_info.user) != creator_address
|
||||
):
|
||||
|
||||
if creator_address and str(token_info.user) != creator_address:
|
||||
logger.info(
|
||||
f"Token not created by {creator_address}. Skipping..."
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
await token_callback(token_info)
|
||||
|
||||
|
||||
except grpc.aio.AioRpcError as e:
|
||||
logger.error(f"gRPC error: {e.details()}")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
|
||||
finally:
|
||||
await channel.close()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Geyser connection error: {e}")
|
||||
logger.info("Reconnecting in 10 seconds...")
|
||||
await asyncio.sleep(10)
|
||||
|
||||
|
||||
async def _process_update(self, update) -> TokenInfo | None:
|
||||
"""Process a Geyser update and extract token creation info.
|
||||
|
||||
|
||||
Args:
|
||||
update: Geyser update from the subscription
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if a token creation is found, None otherwise
|
||||
"""
|
||||
try:
|
||||
if not update.HasField("transaction"):
|
||||
return None
|
||||
|
||||
|
||||
tx = update.transaction.transaction.transaction
|
||||
msg = getattr(tx, "message", None)
|
||||
if msg is None:
|
||||
@@ -151,20 +160,20 @@ class GeyserListener(BaseTokenListener):
|
||||
program_idx = ix.program_id_index
|
||||
if program_idx >= len(msg.account_keys):
|
||||
continue
|
||||
|
||||
|
||||
program_id = msg.account_keys[program_idx]
|
||||
if bytes(program_id) != bytes(self.pump_program):
|
||||
continue
|
||||
|
||||
|
||||
# Process instruction data
|
||||
token_info = self.event_processor.process_transaction_data(
|
||||
ix.data, ix.accounts, msg.account_keys
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing Geyser update: {e}")
|
||||
return None
|
||||
|
||||
@@ -43,7 +43,7 @@ class LogsEventProcessor:
|
||||
# Check if this is a token creation
|
||||
if not any("Program log: Instruction: Create" in log for log in logs):
|
||||
return None
|
||||
|
||||
|
||||
# Skip swaps as the first condition may pass them
|
||||
if any("Program log: Instruction: CreateTokenAccount" in log for log in logs):
|
||||
return None
|
||||
@@ -55,7 +55,7 @@ class LogsEventProcessor:
|
||||
encoded_data = log.split(": ")[1]
|
||||
decoded_data = base64.b64decode(encoded_data)
|
||||
parsed_data = self._parse_create_instruction(decoded_data)
|
||||
|
||||
|
||||
if parsed_data and "name" in parsed_data:
|
||||
mint = Pubkey.from_string(parsed_data["mint"])
|
||||
bonding_curve = Pubkey.from_string(parsed_data["bondingCurve"])
|
||||
@@ -64,7 +64,7 @@ class LogsEventProcessor:
|
||||
)
|
||||
creator = Pubkey.from_string(parsed_data["creator"])
|
||||
creator_vault = self._find_creator_vault(creator)
|
||||
|
||||
|
||||
return TokenInfo(
|
||||
name=parsed_data["name"],
|
||||
symbol=parsed_data["symbol"],
|
||||
@@ -78,7 +78,7 @@ class LogsEventProcessor:
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process log data: {e}")
|
||||
|
||||
|
||||
return None
|
||||
|
||||
def _parse_create_instruction(self, data: bytes) -> dict | None:
|
||||
@@ -92,11 +92,13 @@ class LogsEventProcessor:
|
||||
"""
|
||||
if len(data) < 8:
|
||||
return None
|
||||
|
||||
|
||||
# Check for the correct instruction discriminator
|
||||
discriminator = struct.unpack("<Q", data[:8])[0]
|
||||
if discriminator != self.CREATE_DISCRIMINATOR:
|
||||
logger.info(f"Skipping non-Create instruction with discriminator: {discriminator}")
|
||||
logger.info(
|
||||
f"Skipping non-Create instruction with discriminator: {discriminator}"
|
||||
)
|
||||
return None
|
||||
|
||||
offset = 8
|
||||
@@ -154,7 +156,7 @@ class LogsEventProcessor:
|
||||
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
|
||||
|
||||
def _find_creator_vault(self, creator: Pubkey) -> Pubkey:
|
||||
"""
|
||||
Find the creator vault for a creator.
|
||||
@@ -166,10 +168,7 @@ class LogsEventProcessor:
|
||||
Creator vault address
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
b"creator-vault",
|
||||
bytes(creator)
|
||||
],
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpAddresses.PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
|
||||
@@ -55,7 +55,7 @@ class PumpPortalEventProcessor:
|
||||
mint = Pubkey.from_string(mint_str)
|
||||
bonding_curve = Pubkey.from_string(bonding_curve_str)
|
||||
user = Pubkey.from_string(creator_str)
|
||||
|
||||
|
||||
# For PumpPortal, we assume the creator is the same as the user
|
||||
# since PumpPortal doesn't distinguish between them
|
||||
creator = user
|
||||
@@ -117,10 +117,7 @@ class PumpPortalEventProcessor:
|
||||
Creator vault address
|
||||
"""
|
||||
derived_address, _ = Pubkey.find_program_address(
|
||||
[
|
||||
b"creator-vault",
|
||||
bytes(creator)
|
||||
],
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpAddresses.PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
return derived_address
|
||||
|
||||
@@ -20,7 +20,11 @@ logger = get_logger(__name__)
|
||||
class PumpPortalListener(BaseTokenListener):
|
||||
"""PumpPortal listener for pump.fun token creation events."""
|
||||
|
||||
def __init__(self, pump_program: Pubkey, pumpportal_url: str = "wss://pumpportal.fun/api/data"):
|
||||
def __init__(
|
||||
self,
|
||||
pump_program: Pubkey,
|
||||
pumpportal_url: str = "wss://pumpportal.fun/api/data",
|
||||
):
|
||||
"""Initialize token listener.
|
||||
|
||||
Args:
|
||||
@@ -82,7 +86,9 @@ class PumpPortalListener(BaseTokenListener):
|
||||
await token_callback(token_info)
|
||||
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.warning("PumpPortal WebSocket connection closed. Reconnecting...")
|
||||
logger.warning(
|
||||
"PumpPortal WebSocket connection closed. Reconnecting..."
|
||||
)
|
||||
finally:
|
||||
ping_task.cancel()
|
||||
try:
|
||||
@@ -94,16 +100,14 @@ class PumpPortalListener(BaseTokenListener):
|
||||
logger.exception("PumpPortal WebSocket connection error")
|
||||
logger.info("Reconnecting in 5 seconds...")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _subscribe_to_new_tokens(self, websocket) -> None:
|
||||
"""Subscribe to new token events from PumpPortal.
|
||||
|
||||
Args:
|
||||
websocket: Active WebSocket connection
|
||||
"""
|
||||
subscription_message = json.dumps({
|
||||
"method": "subscribeNewToken",
|
||||
"params": []
|
||||
})
|
||||
subscription_message = json.dumps({"method": "subscribeNewToken", "params": []})
|
||||
|
||||
await websocket.send(subscription_message)
|
||||
logger.info("Subscribed to PumpPortal new token events")
|
||||
@@ -167,4 +171,4 @@ class PumpPortalListener(BaseTokenListener):
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing PumpPortal WebSocket message: {e}")
|
||||
|
||||
return None
|
||||
return None
|
||||
|
||||
+113
-58
@@ -82,10 +82,12 @@ class TokenBuyer(Trader):
|
||||
# Skip the wait and directly calculate the amount
|
||||
token_amount = self.extreme_fast_token_amount
|
||||
token_price_sol = self.amount / token_amount
|
||||
#logger.info(f"EXTREME FAST Mode: Buying {token_amount} tokens.")
|
||||
# logger.info(f"EXTREME FAST Mode: Buying {token_amount} tokens.")
|
||||
else:
|
||||
# Regular behavior with RPC call
|
||||
curve_state = await self.curve_manager.get_curve_state(token_info.bonding_curve)
|
||||
curve_state = await self.curve_manager.get_curve_state(
|
||||
token_info.bonding_curve
|
||||
)
|
||||
token_price_sol = curve_state.calculate_price()
|
||||
token_amount = self.amount / token_price_sol
|
||||
|
||||
@@ -114,16 +116,20 @@ class TokenBuyer(Trader):
|
||||
|
||||
if success:
|
||||
# Get actual execution data from bonding curve balance changes
|
||||
actual_price, actual_tokens = await self._get_actual_execution_price(tx_signature, token_info)
|
||||
|
||||
actual_price, actual_tokens = await self._get_actual_execution_price(
|
||||
tx_signature, token_info
|
||||
)
|
||||
|
||||
logger.info(f"Buy transaction confirmed: {tx_signature}")
|
||||
logger.info(f"Actual price paid to bonding curve: {actual_price:.8f} SOL per token")
|
||||
|
||||
logger.info(
|
||||
f"Actual price paid to bonding curve: {actual_price:.8f} SOL per token"
|
||||
)
|
||||
|
||||
return TradeResult(
|
||||
success=True,
|
||||
tx_signature=tx_signature,
|
||||
amount=actual_tokens, # Actual tokens received
|
||||
price=actual_price, # Actual price based on bonding curve SOL flow
|
||||
amount=actual_tokens, # Actual tokens received
|
||||
price=actual_price, # Actual price based on bonding curve SOL flow
|
||||
)
|
||||
else:
|
||||
return TradeResult(
|
||||
@@ -196,7 +202,7 @@ class TokenBuyer(Trader):
|
||||
self.wallet.pubkey,
|
||||
self.wallet.pubkey,
|
||||
token_info.mint,
|
||||
SystemAddresses.TOKEN_PROGRAM
|
||||
SystemAddresses.TOKEN_PROGRAM,
|
||||
)
|
||||
|
||||
# Prepare buy instruction data
|
||||
@@ -222,129 +228,178 @@ class TokenBuyer(Trader):
|
||||
logger.error(f"Buy transaction failed: {e!s}")
|
||||
raise
|
||||
|
||||
|
||||
async def _get_actual_execution_price(self, tx_signature: str, token_info: TokenInfo) -> tuple[float, float]:
|
||||
async def _get_actual_execution_price(
|
||||
self, tx_signature: str, token_info: TokenInfo
|
||||
) -> tuple[float, float]:
|
||||
"""Get actual execution price from bonding curve SOL balance changes."""
|
||||
try:
|
||||
client = await self.client.get_client()
|
||||
|
||||
tx_response = await client.get_transaction(
|
||||
tx_signature,
|
||||
tx_signature,
|
||||
encoding="jsonParsed",
|
||||
commitment="confirmed",
|
||||
max_supported_transaction_version=0
|
||||
max_supported_transaction_version=0,
|
||||
)
|
||||
|
||||
|
||||
if not tx_response.value or not tx_response.value.transaction:
|
||||
raise ValueError("Transaction not found")
|
||||
|
||||
|
||||
meta = tx_response.value.transaction.meta
|
||||
if not meta or not meta.pre_balances or not meta.post_balances:
|
||||
raise ValueError("Transaction balance data not found")
|
||||
|
||||
|
||||
# Get accounts - they're ParsedAccountTxStatus objects, need to extract pubkey
|
||||
accounts = tx_response.value.transaction.transaction.message.account_keys
|
||||
|
||||
|
||||
# Find bonding curve account index in the transaction
|
||||
bonding_curve_index = None
|
||||
for i, account in enumerate(accounts):
|
||||
# Extract pubkey from ParsedAccountTxStatus object
|
||||
account_pubkey = str(account.pubkey) if hasattr(account, 'pubkey') else str(account)
|
||||
|
||||
account_pubkey = (
|
||||
str(account.pubkey) if hasattr(account, "pubkey") else str(account)
|
||||
)
|
||||
|
||||
if account_pubkey == str(token_info.bonding_curve):
|
||||
bonding_curve_index = i
|
||||
break
|
||||
|
||||
|
||||
if bonding_curve_index is None:
|
||||
raise ValueError("Bonding curve not found in transaction accounts")
|
||||
|
||||
|
||||
pre_balance_lamports = meta.pre_balances[bonding_curve_index]
|
||||
post_balance_lamports = meta.post_balances[bonding_curve_index]
|
||||
|
||||
sol_sent_to_curve = (post_balance_lamports - pre_balance_lamports) / LAMPORTS_PER_SOL
|
||||
|
||||
sol_sent_to_curve = (
|
||||
post_balance_lamports - pre_balance_lamports
|
||||
) / LAMPORTS_PER_SOL
|
||||
|
||||
if sol_sent_to_curve <= 0:
|
||||
raise ValueError(f"No SOL sent to bonding curve: {sol_sent_to_curve}")
|
||||
|
||||
tokens_received = await self._get_tokens_received_from_tx(tx_response, token_info)
|
||||
|
||||
|
||||
tokens_received = await self._get_tokens_received_from_tx(
|
||||
tx_response, token_info
|
||||
)
|
||||
|
||||
if tokens_received == 0:
|
||||
raise ValueError("Cannot compute execution price: zero tokens received")
|
||||
actual_price = sol_sent_to_curve / tokens_received
|
||||
|
||||
|
||||
logger.info(f"Bonding curve received: {sol_sent_to_curve:.6f} SOL")
|
||||
logger.info(f"We received: {tokens_received:.6f} tokens")
|
||||
logger.info(f"Actual execution price: {actual_price:.8f} SOL per token")
|
||||
|
||||
|
||||
return actual_price, tokens_received
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to get actual execution price from bonding curve: {e}")
|
||||
logger.warning(
|
||||
f"Failed to get actual execution price from bonding curve: {e}"
|
||||
)
|
||||
# Fallback to EXTREME_FAST estimate
|
||||
tokens_received = self.extreme_fast_token_amount if self.extreme_fast_mode else self.amount / await self.curve_manager.calculate_price(token_info.bonding_curve)
|
||||
tokens_received = (
|
||||
self.extreme_fast_token_amount
|
||||
if self.extreme_fast_mode
|
||||
else self.amount
|
||||
/ await self.curve_manager.calculate_price(token_info.bonding_curve)
|
||||
)
|
||||
if tokens_received == 0:
|
||||
logger.error("Fallback failed – unable to determine tokens received")
|
||||
return 0.0, 0.0
|
||||
logger.error("Fallback failed – unable to determine tokens received")
|
||||
return 0.0, 0.0
|
||||
return self.amount / tokens_received, tokens_received
|
||||
|
||||
|
||||
async def _get_tokens_received_from_tx(self, tx_response, token_info: TokenInfo) -> float:
|
||||
async def _get_tokens_received_from_tx(
|
||||
self, tx_response, token_info: TokenInfo
|
||||
) -> float:
|
||||
"""Extract tokens received from transaction token balance changes."""
|
||||
meta = tx_response.value.transaction.meta
|
||||
|
||||
|
||||
pre_token_balance = 0
|
||||
post_token_balance = 0
|
||||
|
||||
|
||||
wallet_str = str(self.wallet.pubkey)
|
||||
mint_str = str(token_info.mint)
|
||||
|
||||
|
||||
if meta.pre_token_balances:
|
||||
for balance in meta.pre_token_balances:
|
||||
# Convert to string for comparison
|
||||
balance_owner = str(balance.owner) if hasattr(balance, 'owner') else str(getattr(balance, 'owner', ''))
|
||||
balance_mint = str(balance.mint) if hasattr(balance, 'mint') else str(getattr(balance, 'mint', ''))
|
||||
|
||||
balance_owner = (
|
||||
str(balance.owner)
|
||||
if hasattr(balance, "owner")
|
||||
else str(getattr(balance, "owner", ""))
|
||||
)
|
||||
balance_mint = (
|
||||
str(balance.mint)
|
||||
if hasattr(balance, "mint")
|
||||
else str(getattr(balance, "mint", ""))
|
||||
)
|
||||
|
||||
if balance_owner == wallet_str and balance_mint == mint_str:
|
||||
try:
|
||||
# Try multiple ways to get the amount
|
||||
if hasattr(balance, 'ui_token_amount'):
|
||||
if hasattr(balance, "ui_token_amount"):
|
||||
amount_obj = balance.ui_token_amount
|
||||
if hasattr(amount_obj, 'amount') and amount_obj.amount is not None:
|
||||
if (
|
||||
hasattr(amount_obj, "amount")
|
||||
and amount_obj.amount is not None
|
||||
):
|
||||
pre_token_balance = int(amount_obj.amount)
|
||||
elif hasattr(amount_obj, 'ui_amount') and amount_obj.ui_amount is not None:
|
||||
pre_token_balance = int(float(amount_obj.ui_amount) * (10**TOKEN_DECIMALS))
|
||||
elif (
|
||||
hasattr(amount_obj, "ui_amount")
|
||||
and amount_obj.ui_amount is not None
|
||||
):
|
||||
pre_token_balance = int(
|
||||
float(amount_obj.ui_amount) * (10**TOKEN_DECIMALS)
|
||||
)
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning(f"Error parsing pre-token balance: {e}")
|
||||
break
|
||||
|
||||
# Check post-token balances
|
||||
|
||||
# Check post-token balances
|
||||
if meta.post_token_balances:
|
||||
for balance in meta.post_token_balances:
|
||||
# Convert to string for comparison
|
||||
balance_owner = str(balance.owner) if hasattr(balance, 'owner') else str(getattr(balance, 'owner', ''))
|
||||
balance_mint = str(balance.mint) if hasattr(balance, 'mint') else str(getattr(balance, 'mint', ''))
|
||||
|
||||
balance_owner = (
|
||||
str(balance.owner)
|
||||
if hasattr(balance, "owner")
|
||||
else str(getattr(balance, "owner", ""))
|
||||
)
|
||||
balance_mint = (
|
||||
str(balance.mint)
|
||||
if hasattr(balance, "mint")
|
||||
else str(getattr(balance, "mint", ""))
|
||||
)
|
||||
|
||||
if balance_owner == wallet_str and balance_mint == mint_str:
|
||||
try:
|
||||
# Try multiple ways to get the amount
|
||||
if hasattr(balance, 'ui_token_amount'):
|
||||
if hasattr(balance, "ui_token_amount"):
|
||||
amount_obj = balance.ui_token_amount
|
||||
if hasattr(amount_obj, 'amount') and amount_obj.amount is not None:
|
||||
if (
|
||||
hasattr(amount_obj, "amount")
|
||||
and amount_obj.amount is not None
|
||||
):
|
||||
post_token_balance = int(amount_obj.amount)
|
||||
elif hasattr(amount_obj, 'ui_amount') and amount_obj.ui_amount is not None:
|
||||
post_token_balance = int(float(amount_obj.ui_amount) * (10**TOKEN_DECIMALS))
|
||||
elif (
|
||||
hasattr(amount_obj, "ui_amount")
|
||||
and amount_obj.ui_amount is not None
|
||||
):
|
||||
post_token_balance = int(
|
||||
float(amount_obj.ui_amount) * (10**TOKEN_DECIMALS)
|
||||
)
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.warning(f"Error parsing post-token balance: {e}")
|
||||
break
|
||||
|
||||
|
||||
# Calculate tokens received
|
||||
if pre_token_balance == 0 and post_token_balance > 0:
|
||||
tokens_received_raw = post_token_balance
|
||||
else:
|
||||
tokens_received_raw = post_token_balance - pre_token_balance
|
||||
|
||||
|
||||
if tokens_received_raw <= 0:
|
||||
logger.warning("Token balance search failed. Using fallback from EXTREME_FAST estimate.")
|
||||
logger.warning(
|
||||
"Token balance search failed. Using fallback from EXTREME_FAST estimate."
|
||||
)
|
||||
# Fallback: use the amount we know we bought
|
||||
if self.extreme_fast_mode and self.extreme_fast_token_amount > 0:
|
||||
return self.extreme_fast_token_amount
|
||||
@@ -352,4 +407,4 @@ class TokenBuyer(Trader):
|
||||
logger.error("Cannot determine tokens received from transaction")
|
||||
return 0.0
|
||||
|
||||
return tokens_received_raw / 10**TOKEN_DECIMALS
|
||||
return tokens_received_raw / 10**TOKEN_DECIMALS
|
||||
|
||||
+27
-26
@@ -11,6 +11,7 @@ from solders.pubkey import Pubkey
|
||||
|
||||
class ExitReason(Enum):
|
||||
"""Reasons for position exit."""
|
||||
|
||||
TAKE_PROFIT = "take_profit"
|
||||
STOP_LOSS = "stop_loss"
|
||||
MAX_HOLD_TIME = "max_hold_time"
|
||||
@@ -20,27 +21,27 @@ class ExitReason(Enum):
|
||||
@dataclass
|
||||
class Position:
|
||||
"""Represents an active trading position."""
|
||||
|
||||
|
||||
# Token information
|
||||
mint: Pubkey
|
||||
symbol: str
|
||||
|
||||
|
||||
# Position details
|
||||
entry_price: float
|
||||
quantity: float
|
||||
entry_time: datetime
|
||||
|
||||
|
||||
# Exit conditions
|
||||
take_profit_price: float | None = None
|
||||
stop_loss_price: float | None = None
|
||||
max_hold_time: int | None = None # seconds
|
||||
|
||||
|
||||
# Status
|
||||
is_active: bool = True
|
||||
exit_reason: ExitReason | None = None
|
||||
exit_price: float | None = None
|
||||
exit_time: datetime | None = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def create_from_buy_result(
|
||||
cls,
|
||||
@@ -53,7 +54,7 @@ class Position:
|
||||
max_hold_time: int | None = None,
|
||||
) -> "Position":
|
||||
"""Create a position from a successful buy transaction.
|
||||
|
||||
|
||||
Args:
|
||||
mint: Token mint address
|
||||
symbol: Token symbol
|
||||
@@ -62,18 +63,18 @@ class Position:
|
||||
take_profit_percentage: Take profit percentage (0.5 = 50% profit)
|
||||
stop_loss_percentage: Stop loss percentage (0.2 = 20% loss)
|
||||
max_hold_time: Maximum hold time in seconds
|
||||
|
||||
|
||||
Returns:
|
||||
Position instance
|
||||
"""
|
||||
take_profit_price = None
|
||||
if take_profit_percentage is not None:
|
||||
take_profit_price = entry_price * (1 + take_profit_percentage)
|
||||
|
||||
|
||||
stop_loss_price = None
|
||||
if stop_loss_percentage is not None:
|
||||
stop_loss_price = entry_price * (1 - stop_loss_percentage)
|
||||
|
||||
|
||||
return cls(
|
||||
mint=mint,
|
||||
symbol=symbol,
|
||||
@@ -84,38 +85,38 @@ class Position:
|
||||
stop_loss_price=stop_loss_price,
|
||||
max_hold_time=max_hold_time,
|
||||
)
|
||||
|
||||
|
||||
def should_exit(self, current_price: float) -> tuple[bool, ExitReason | None]:
|
||||
"""Check if position should be exited based on current conditions.
|
||||
|
||||
|
||||
Args:
|
||||
current_price: Current token price
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (should_exit, exit_reason)
|
||||
"""
|
||||
if not self.is_active:
|
||||
return False, None
|
||||
|
||||
|
||||
# Check take profit
|
||||
if self.take_profit_price and current_price >= self.take_profit_price:
|
||||
return True, ExitReason.TAKE_PROFIT
|
||||
|
||||
|
||||
# Check stop loss
|
||||
if self.stop_loss_price and current_price <= self.stop_loss_price:
|
||||
return True, ExitReason.STOP_LOSS
|
||||
|
||||
|
||||
# Check max hold time
|
||||
if self.max_hold_time:
|
||||
elapsed_time = (datetime.utcnow() - self.entry_time).total_seconds()
|
||||
if elapsed_time >= self.max_hold_time:
|
||||
return True, ExitReason.MAX_HOLD_TIME
|
||||
|
||||
|
||||
return False, None
|
||||
|
||||
|
||||
def close_position(self, exit_price: float, exit_reason: ExitReason) -> None:
|
||||
"""Close the position with exit details.
|
||||
|
||||
|
||||
Args:
|
||||
exit_price: Price at which position was exited
|
||||
exit_reason: Reason for exit
|
||||
@@ -124,27 +125,27 @@ class Position:
|
||||
self.exit_price = exit_price
|
||||
self.exit_reason = exit_reason
|
||||
self.exit_time = datetime.utcnow()
|
||||
|
||||
|
||||
def get_pnl(self, current_price: float | None = None) -> dict:
|
||||
"""Calculate profit/loss for the position.
|
||||
|
||||
|
||||
Args:
|
||||
current_price: Current price (uses exit_price if position is closed)
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary with PnL information
|
||||
"""
|
||||
if self.is_active and current_price is None:
|
||||
raise ValueError("current_price required for active position")
|
||||
|
||||
|
||||
price_to_use = self.exit_price if not self.is_active else current_price
|
||||
if price_to_use is None:
|
||||
raise ValueError("No price available for PnL calculation")
|
||||
|
||||
|
||||
price_change = price_to_use - self.entry_price
|
||||
price_change_pct = (price_change / self.entry_price) * 100
|
||||
unrealized_pnl = price_change * self.quantity
|
||||
|
||||
|
||||
return {
|
||||
"entry_price": self.entry_price,
|
||||
"current_price": price_to_use,
|
||||
@@ -153,7 +154,7 @@ class Position:
|
||||
"unrealized_pnl_sol": unrealized_pnl,
|
||||
"quantity": self.quantity,
|
||||
}
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""String representation of position."""
|
||||
if self.is_active:
|
||||
@@ -162,4 +163,4 @@ class Position:
|
||||
status = f"CLOSED ({self.exit_reason.value})"
|
||||
else:
|
||||
status = "CLOSED (UNKNOWN)"
|
||||
return f"Position({self.symbol}: {self.quantity:.6f} @ {self.entry_price:.8f} SOL - {status})"
|
||||
return f"Position({self.symbol}: {self.quantity:.6f} @ {self.entry_price:.8f} SOL - {status})"
|
||||
|
||||
@@ -175,7 +175,9 @@ class TokenSeller(Trader):
|
||||
pubkey=SystemAddresses.PROGRAM, is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=token_info.creator_vault, is_signer=False, is_writable=True,
|
||||
pubkey=token_info.creator_vault,
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
|
||||
|
||||
+134
-107
@@ -39,6 +39,7 @@ logger = get_logger(__name__)
|
||||
|
||||
class PumpTrader:
|
||||
"""Coordinates trading operations for pump.fun tokens with focus on freshness."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rpc_endpoint: str,
|
||||
@@ -52,37 +53,31 @@ class PumpTrader:
|
||||
geyser_api_token: str | None = None,
|
||||
geyser_auth_type: str = "x-token",
|
||||
pumpportal_url: str = "wss://pumpportal.fun/api/data",
|
||||
|
||||
extreme_fast_mode: bool = False,
|
||||
extreme_fast_token_amount: int = 30,
|
||||
|
||||
# Exit strategy configuration
|
||||
exit_strategy: str = "time_based",
|
||||
take_profit_percentage: float | None = None,
|
||||
stop_loss_percentage: float | None = None,
|
||||
max_hold_time: int | None = None,
|
||||
price_check_interval: int = 10,
|
||||
|
||||
# Priority fee configuration
|
||||
enable_dynamic_priority_fee: bool = False,
|
||||
enable_fixed_priority_fee: bool = True,
|
||||
fixed_priority_fee: int = 200_000,
|
||||
extra_priority_fee: float = 0.0,
|
||||
hard_cap_prior_fee: int = 200_000,
|
||||
|
||||
# Retry and timeout settings
|
||||
max_retries: int = 3,
|
||||
wait_time_after_creation: int = 15, # here and further - seconds
|
||||
wait_time_after_creation: int = 15, # here and further - seconds
|
||||
wait_time_after_buy: int = 15,
|
||||
wait_time_before_new_token: int = 15,
|
||||
max_token_age: int | float = 0.001,
|
||||
token_wait_timeout: int = 30,
|
||||
|
||||
# Cleanup settings
|
||||
cleanup_mode: str = "disabled",
|
||||
cleanup_force_close_with_burn: bool = False,
|
||||
cleanup_with_priority_fee: bool = False,
|
||||
|
||||
# Trading filters
|
||||
match_string: str | None = None,
|
||||
bro_address: str | None = None,
|
||||
@@ -129,7 +124,7 @@ class PumpTrader:
|
||||
cleanup_mode: Cleanup mode ("disabled", "auto", or "manual")
|
||||
cleanup_force_close_with_burn: Whether to force close with burn during cleanup
|
||||
cleanup_with_priority_fee: Whether to use priority fees during cleanup
|
||||
|
||||
|
||||
match_string: Optional string to match in token name/symbol
|
||||
bro_address: Optional creator address to filter by
|
||||
marry_mode: If True, only buy tokens and skip selling
|
||||
@@ -155,7 +150,7 @@ class PumpTrader:
|
||||
buy_slippage,
|
||||
max_retries,
|
||||
extreme_fast_token_amount,
|
||||
extreme_fast_mode
|
||||
extreme_fast_mode,
|
||||
)
|
||||
self.seller = TokenSeller(
|
||||
self.solana_client,
|
||||
@@ -165,30 +160,34 @@ class PumpTrader:
|
||||
sell_slippage,
|
||||
max_retries,
|
||||
)
|
||||
|
||||
|
||||
# Initialize the appropriate listener type
|
||||
listener_type = listener_type.lower()
|
||||
if listener_type == "geyser":
|
||||
if not geyser_endpoint or not geyser_api_token:
|
||||
raise ValueError("Geyser endpoint and API token are required for geyser listener")
|
||||
|
||||
raise ValueError(
|
||||
"Geyser endpoint and API token are required for geyser listener"
|
||||
)
|
||||
|
||||
self.token_listener = GeyserListener(
|
||||
geyser_endpoint,
|
||||
geyser_endpoint,
|
||||
geyser_api_token,
|
||||
geyser_auth_type,
|
||||
PumpAddresses.PROGRAM
|
||||
geyser_auth_type,
|
||||
PumpAddresses.PROGRAM,
|
||||
)
|
||||
logger.info("Using Geyser listener for token monitoring")
|
||||
elif listener_type == "logs":
|
||||
self.token_listener = LogsListener(wss_endpoint, PumpAddresses.PROGRAM)
|
||||
logger.info("Using logsSubscribe listener for token monitoring")
|
||||
elif listener_type == "pumpportal":
|
||||
self.token_listener = PumpPortalListener(PumpAddresses.PROGRAM, pumpportal_url)
|
||||
self.token_listener = PumpPortalListener(
|
||||
PumpAddresses.PROGRAM, pumpportal_url
|
||||
)
|
||||
logger.info("Using PumpPortal listener for token monitoring")
|
||||
else:
|
||||
self.token_listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM)
|
||||
logger.info("Using blockSubscribe listener for token monitoring")
|
||||
|
||||
|
||||
# Trading parameters
|
||||
self.buy_amount = buy_amount
|
||||
self.buy_slippage = buy_slippage
|
||||
@@ -196,21 +195,21 @@ class PumpTrader:
|
||||
self.max_retries = max_retries
|
||||
self.extreme_fast_mode = extreme_fast_mode
|
||||
self.extreme_fast_token_amount = extreme_fast_token_amount
|
||||
|
||||
|
||||
# Exit strategy parameters
|
||||
self.exit_strategy = exit_strategy.lower()
|
||||
self.take_profit_percentage = take_profit_percentage
|
||||
self.stop_loss_percentage = stop_loss_percentage
|
||||
self.max_hold_time = max_hold_time
|
||||
self.price_check_interval = price_check_interval
|
||||
|
||||
|
||||
# Timing parameters
|
||||
self.wait_time_after_creation = wait_time_after_creation
|
||||
self.wait_time_after_buy = wait_time_after_buy
|
||||
self.wait_time_before_new_token = wait_time_before_new_token
|
||||
self.max_token_age = max_token_age
|
||||
self.token_wait_timeout = token_wait_timeout
|
||||
|
||||
|
||||
# Cleanup parameters
|
||||
self.cleanup_mode = cleanup_mode
|
||||
self.cleanup_force_close_with_burn = cleanup_force_close_with_burn
|
||||
@@ -221,26 +220,36 @@ class PumpTrader:
|
||||
self.bro_address = bro_address
|
||||
self.marry_mode = marry_mode
|
||||
self.yolo_mode = yolo_mode
|
||||
|
||||
|
||||
# State tracking
|
||||
self.traded_mints: set[Pubkey] = set()
|
||||
self.token_queue: asyncio.Queue = asyncio.Queue()
|
||||
self.processing: bool = False
|
||||
self.processed_tokens: set[str] = set()
|
||||
self.token_timestamps: dict[str, float] = {}
|
||||
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Start the trading bot and listen for new tokens."""
|
||||
logger.info("Starting pump.fun trader")
|
||||
logger.info(f"Match filter: {self.match_string if self.match_string else 'None'}")
|
||||
logger.info(f"Creator filter: {self.bro_address if self.bro_address else 'None'}")
|
||||
logger.info(
|
||||
f"Match filter: {self.match_string if self.match_string else 'None'}"
|
||||
)
|
||||
logger.info(
|
||||
f"Creator filter: {self.bro_address if self.bro_address else 'None'}"
|
||||
)
|
||||
logger.info(f"Marry mode: {self.marry_mode}")
|
||||
logger.info(f"YOLO mode: {self.yolo_mode}")
|
||||
logger.info(f"Exit strategy: {self.exit_strategy}")
|
||||
if self.exit_strategy == "tp_sl":
|
||||
logger.info(f"Take profit: {self.take_profit_percentage * 100 if self.take_profit_percentage else 'None'}%")
|
||||
logger.info(f"Stop loss: {self.stop_loss_percentage * 100 if self.stop_loss_percentage else 'None'}%")
|
||||
logger.info(f"Max hold time: {self.max_hold_time if self.max_hold_time else 'None'} seconds")
|
||||
logger.info(
|
||||
f"Take profit: {self.take_profit_percentage * 100 if self.take_profit_percentage else 'None'}%"
|
||||
)
|
||||
logger.info(
|
||||
f"Stop loss: {self.stop_loss_percentage * 100 if self.stop_loss_percentage else 'None'}%"
|
||||
)
|
||||
logger.info(
|
||||
f"Max hold time: {self.max_hold_time if self.max_hold_time else 'None'} seconds"
|
||||
)
|
||||
logger.info(f"Max token age: {self.max_token_age} seconds")
|
||||
|
||||
try:
|
||||
@@ -253,19 +262,23 @@ class PumpTrader:
|
||||
# Choose operating mode based on yolo_mode
|
||||
if not self.yolo_mode:
|
||||
# Single token mode: process one token and exit
|
||||
logger.info("Running in single token mode - will process one token and exit")
|
||||
logger.info(
|
||||
"Running in single token mode - will process one token and exit"
|
||||
)
|
||||
token_info = await self._wait_for_token()
|
||||
if token_info:
|
||||
await self._handle_token(token_info)
|
||||
logger.info("Finished processing single token. Exiting...")
|
||||
else:
|
||||
logger.info(f"No suitable token found within timeout period ({self.token_wait_timeout}s). Exiting...")
|
||||
logger.info(
|
||||
f"No suitable token found within timeout period ({self.token_wait_timeout}s). Exiting..."
|
||||
)
|
||||
else:
|
||||
# Continuous mode: process tokens until interrupted
|
||||
logger.info("Running in continuous mode - will process tokens until interrupted")
|
||||
processor_task = asyncio.create_task(
|
||||
self._process_token_queue()
|
||||
logger.info(
|
||||
"Running in continuous mode - will process tokens until interrupted"
|
||||
)
|
||||
processor_task = asyncio.create_task(self._process_token_queue())
|
||||
|
||||
try:
|
||||
await self.token_listener.listen_for_tokens(
|
||||
@@ -281,28 +294,28 @@ class PumpTrader:
|
||||
await processor_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Trading stopped due to error: {e!s}")
|
||||
|
||||
|
||||
finally:
|
||||
await self._cleanup_resources()
|
||||
logger.info("Pump trader has shut down")
|
||||
|
||||
async def _wait_for_token(self) -> TokenInfo | None:
|
||||
"""Wait for a single token to be detected.
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo or None if timeout occurs
|
||||
"""
|
||||
# Create a one-time event to signal when a token is found
|
||||
token_found = asyncio.Event()
|
||||
found_token = None
|
||||
|
||||
|
||||
async def token_callback(token: TokenInfo) -> None:
|
||||
nonlocal found_token
|
||||
token_key = str(token.mint)
|
||||
|
||||
|
||||
# Only process if not already processed and fresh
|
||||
if token_key not in self.processed_tokens:
|
||||
# Record when the token was discovered
|
||||
@@ -310,7 +323,7 @@ class PumpTrader:
|
||||
found_token = token
|
||||
self.processed_tokens.add(token_key)
|
||||
token_found.set()
|
||||
|
||||
|
||||
listener_task = asyncio.create_task(
|
||||
self.token_listener.listen_for_tokens(
|
||||
token_callback,
|
||||
@@ -318,15 +331,19 @@ class PumpTrader:
|
||||
self.bro_address,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Wait for a token with a timeout
|
||||
try:
|
||||
logger.info(f"Waiting for a suitable token (timeout: {self.token_wait_timeout}s)...")
|
||||
logger.info(
|
||||
f"Waiting for a suitable token (timeout: {self.token_wait_timeout}s)..."
|
||||
)
|
||||
await asyncio.wait_for(token_found.wait(), timeout=self.token_wait_timeout)
|
||||
logger.info(f"Found token: {found_token.symbol} ({found_token.mint})")
|
||||
return found_token
|
||||
except TimeoutError:
|
||||
logger.info(f"Timed out after waiting {self.token_wait_timeout}s for a token")
|
||||
logger.info(
|
||||
f"Timed out after waiting {self.token_wait_timeout}s for a token"
|
||||
)
|
||||
return None
|
||||
finally:
|
||||
listener_task.cancel()
|
||||
@@ -341,28 +358,26 @@ class PumpTrader:
|
||||
try:
|
||||
logger.info(f"Cleaning up {len(self.traded_mints)} traded token(s)...")
|
||||
await handle_cleanup_post_session(
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
list(self.traded_mints),
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
list(self.traded_mints),
|
||||
self.priority_fee_manager,
|
||||
self.cleanup_mode,
|
||||
self.cleanup_with_priority_fee,
|
||||
self.cleanup_force_close_with_burn
|
||||
self.cleanup_force_close_with_burn,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error during cleanup: {e!s}")
|
||||
|
||||
|
||||
old_keys = {k for k in self.token_timestamps if k not in self.processed_tokens}
|
||||
for key in old_keys:
|
||||
self.token_timestamps.pop(key, None)
|
||||
|
||||
|
||||
await self.solana_client.close()
|
||||
|
||||
async def _queue_token(
|
||||
self, token_info: TokenInfo
|
||||
) -> None:
|
||||
async def _queue_token(self, token_info: TokenInfo) -> None:
|
||||
"""Queue a token for processing if not already processed.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information to queue
|
||||
"""
|
||||
@@ -413,9 +428,7 @@ class PumpTrader:
|
||||
finally:
|
||||
self.token_queue.task_done()
|
||||
|
||||
async def _handle_token(
|
||||
self, token_info: TokenInfo
|
||||
) -> None:
|
||||
async def _handle_token(self, token_info: TokenInfo) -> None:
|
||||
"""Handle a new token creation event.
|
||||
|
||||
Args:
|
||||
@@ -456,7 +469,7 @@ class PumpTrader:
|
||||
self, token_info: TokenInfo, buy_result: TradeResult
|
||||
) -> None:
|
||||
"""Handle successful token purchase.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
buy_result: The result of the buy operation
|
||||
@@ -470,7 +483,7 @@ class PumpTrader:
|
||||
buy_result.tx_signature,
|
||||
)
|
||||
self.traded_mints.add(token_info.mint)
|
||||
|
||||
|
||||
# Choose exit strategy
|
||||
if not self.marry_mode:
|
||||
if self.exit_strategy == "tp_sl":
|
||||
@@ -486,28 +499,28 @@ class PumpTrader:
|
||||
self, token_info: TokenInfo, buy_result: TradeResult
|
||||
) -> None:
|
||||
"""Handle failed token purchase.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
buy_result: The result of the buy operation
|
||||
"""
|
||||
logger.error(
|
||||
f"Failed to buy {token_info.symbol}: {buy_result.error_message}"
|
||||
)
|
||||
logger.error(f"Failed to buy {token_info.symbol}: {buy_result.error_message}")
|
||||
# Close ATA if enabled
|
||||
await handle_cleanup_after_failure(
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
token_info.mint,
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
token_info.mint,
|
||||
self.priority_fee_manager,
|
||||
self.cleanup_mode,
|
||||
self.cleanup_with_priority_fee,
|
||||
self.cleanup_force_close_with_burn
|
||||
self.cleanup_force_close_with_burn,
|
||||
)
|
||||
|
||||
async def _handle_tp_sl_exit(self, token_info: TokenInfo, buy_result: TradeResult) -> None:
|
||||
async def _handle_tp_sl_exit(
|
||||
self, token_info: TokenInfo, buy_result: TradeResult
|
||||
) -> None:
|
||||
"""Handle take profit/stop loss exit strategy.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
buy_result: Result from the buy operation
|
||||
@@ -517,30 +530,28 @@ class PumpTrader:
|
||||
mint=token_info.mint,
|
||||
symbol=token_info.symbol,
|
||||
entry_price=buy_result.price, # type: ignore
|
||||
quantity=buy_result.amount, # type: ignore
|
||||
quantity=buy_result.amount, # type: ignore
|
||||
take_profit_percentage=self.take_profit_percentage,
|
||||
stop_loss_percentage=self.stop_loss_percentage,
|
||||
max_hold_time=self.max_hold_time,
|
||||
)
|
||||
|
||||
|
||||
logger.info(f"Created position: {position}")
|
||||
if position.take_profit_price:
|
||||
logger.info(f"Take profit target: {position.take_profit_price:.8f} SOL")
|
||||
if position.stop_loss_price:
|
||||
logger.info(f"Stop loss target: {position.stop_loss_price:.8f} SOL")
|
||||
|
||||
|
||||
# Monitor position until exit condition is met
|
||||
await self._monitor_position_until_exit(token_info, position)
|
||||
|
||||
async def _handle_time_based_exit(self, token_info: TokenInfo) -> None:
|
||||
"""Handle legacy time-based exit strategy.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
"""
|
||||
logger.info(
|
||||
f"Waiting for {self.wait_time_after_buy} seconds before selling..."
|
||||
)
|
||||
logger.info(f"Waiting for {self.wait_time_after_buy} seconds before selling...")
|
||||
await asyncio.sleep(self.wait_time_after_buy)
|
||||
|
||||
logger.info(f"Selling {token_info.symbol}...")
|
||||
@@ -557,52 +568,62 @@ class PumpTrader:
|
||||
)
|
||||
# Close ATA if enabled
|
||||
await handle_cleanup_after_sell(
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
token_info.mint,
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
token_info.mint,
|
||||
self.priority_fee_manager,
|
||||
self.cleanup_mode,
|
||||
self.cleanup_with_priority_fee,
|
||||
self.cleanup_force_close_with_burn
|
||||
self.cleanup_force_close_with_burn,
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"Failed to sell {token_info.symbol}: {sell_result.error_message}"
|
||||
)
|
||||
|
||||
async def _monitor_position_until_exit(self, token_info: TokenInfo, position: Position) -> None:
|
||||
async def _monitor_position_until_exit(
|
||||
self, token_info: TokenInfo, position: Position
|
||||
) -> None:
|
||||
"""Monitor a position until exit conditions are met.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
position: Position to monitor
|
||||
"""
|
||||
logger.info(f"Starting position monitoring (check interval: {self.price_check_interval}s)")
|
||||
|
||||
logger.info(
|
||||
f"Starting position monitoring (check interval: {self.price_check_interval}s)"
|
||||
)
|
||||
|
||||
while position.is_active:
|
||||
try:
|
||||
# Get current price from bonding curve
|
||||
current_price = await self.curve_manager.calculate_price(token_info.bonding_curve)
|
||||
|
||||
current_price = await self.curve_manager.calculate_price(
|
||||
token_info.bonding_curve
|
||||
)
|
||||
|
||||
# Check if position should be exited
|
||||
should_exit, exit_reason = position.should_exit(current_price)
|
||||
|
||||
|
||||
if should_exit and exit_reason:
|
||||
logger.info(f"Exit condition met: {exit_reason.value}")
|
||||
logger.info(f"Current price: {current_price:.8f} SOL")
|
||||
|
||||
|
||||
# Log PnL before exit
|
||||
pnl = position.get_pnl(current_price)
|
||||
logger.info(f"Position PnL: {pnl['price_change_pct']:.2f}% ({pnl['unrealized_pnl_sol']:.6f} SOL)")
|
||||
|
||||
logger.info(
|
||||
f"Position PnL: {pnl['price_change_pct']:.2f}% ({pnl['unrealized_pnl_sol']:.6f} SOL)"
|
||||
)
|
||||
|
||||
# Execute sell
|
||||
sell_result = await self.seller.execute(token_info)
|
||||
|
||||
|
||||
if sell_result.success:
|
||||
# Close position with actual exit price
|
||||
position.close_position(sell_result.price, exit_reason) # type: ignore
|
||||
|
||||
logger.info(f"Successfully exited position: {exit_reason.value}")
|
||||
|
||||
logger.info(
|
||||
f"Successfully exited position: {exit_reason.value}"
|
||||
)
|
||||
self._log_trade(
|
||||
"sell",
|
||||
token_info,
|
||||
@@ -610,41 +631,47 @@ class PumpTrader:
|
||||
sell_result.amount, # type: ignore
|
||||
sell_result.tx_signature,
|
||||
)
|
||||
|
||||
|
||||
# Log final PnL
|
||||
final_pnl = position.get_pnl()
|
||||
logger.info(f"Final PnL: {final_pnl['price_change_pct']:.2f}% ({final_pnl['unrealized_pnl_sol']:.6f} SOL)")
|
||||
|
||||
logger.info(
|
||||
f"Final PnL: {final_pnl['price_change_pct']:.2f}% ({final_pnl['unrealized_pnl_sol']:.6f} SOL)"
|
||||
)
|
||||
|
||||
# Close ATA if enabled
|
||||
await handle_cleanup_after_sell(
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
token_info.mint,
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
token_info.mint,
|
||||
self.priority_fee_manager,
|
||||
self.cleanup_mode,
|
||||
self.cleanup_with_priority_fee,
|
||||
self.cleanup_force_close_with_burn
|
||||
self.cleanup_force_close_with_burn,
|
||||
)
|
||||
else:
|
||||
logger.error(f"Failed to exit position: {sell_result.error_message}")
|
||||
logger.error(
|
||||
f"Failed to exit position: {sell_result.error_message}"
|
||||
)
|
||||
# Keep monitoring in case sell can be retried
|
||||
|
||||
|
||||
break
|
||||
else:
|
||||
# Log current status
|
||||
pnl = position.get_pnl(current_price)
|
||||
logger.debug(f"Position status: {current_price:.8f} SOL ({pnl['price_change_pct']:+.2f}%)")
|
||||
|
||||
logger.debug(
|
||||
f"Position status: {current_price:.8f} SOL ({pnl['price_change_pct']:+.2f}%)"
|
||||
)
|
||||
|
||||
# Wait before next price check
|
||||
await asyncio.sleep(self.price_check_interval)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error monitoring position: {e}")
|
||||
await asyncio.sleep(self.price_check_interval) # Continue monitoring despite errors
|
||||
await asyncio.sleep(
|
||||
self.price_check_interval
|
||||
) # Continue monitoring despite errors
|
||||
|
||||
async def _save_token_info(
|
||||
self, token_info: TokenInfo
|
||||
) -> None:
|
||||
async def _save_token_info(self, token_info: TokenInfo) -> None:
|
||||
"""Save token information to a file.
|
||||
|
||||
Args:
|
||||
@@ -694,4 +721,4 @@ class PumpTrader:
|
||||
with open("trades/trades.log", "a") as log_file:
|
||||
log_file.write(json.dumps(log_entry) + "\n")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to log trade information: {e!s}")
|
||||
logger.error(f"Failed to log trade information: {e!s}")
|
||||
|
||||
+4
-1
@@ -43,7 +43,10 @@ def setup_file_logging(
|
||||
|
||||
# Check if file handler with same filename already exists
|
||||
for handler in root_logger.handlers:
|
||||
if isinstance(handler, logging.FileHandler) and handler.baseFilename == filename:
|
||||
if (
|
||||
isinstance(handler, logging.FileHandler)
|
||||
and handler.baseFilename == filename
|
||||
):
|
||||
return # File handler already added
|
||||
|
||||
formatter = logging.Formatter(
|
||||
|
||||
Reference in New Issue
Block a user