mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-07-27 23:37:45 +00:00
docs(claude): add claude code rules
This commit is contained in:
+41
-28
@@ -46,8 +46,11 @@ async def start_bot(config_path: str):
|
||||
# Validate platform support
|
||||
try:
|
||||
from platforms import platform_factory
|
||||
|
||||
if not platform_factory.registry.is_platform_supported(platform):
|
||||
logging.error(f"Platform {platform.value} is not supported. Available platforms: {[p.value for p in platform_factory.get_supported_platforms()]}")
|
||||
logging.error(
|
||||
f"Platform {platform.value} is not supported. Available platforms: {[p.value for p in platform_factory.get_supported_platforms()]}"
|
||||
)
|
||||
return
|
||||
except Exception as e:
|
||||
logging.exception(f"Could not validate platform support: {e}")
|
||||
@@ -57,8 +60,11 @@ async def start_bot(config_path: str):
|
||||
listener_type = cfg["filters"]["listener_type"]
|
||||
if not validate_platform_listener_combination(platform, listener_type):
|
||||
from config_loader import get_supported_listeners_for_platform
|
||||
|
||||
supported = get_supported_listeners_for_platform(platform)
|
||||
logging.error(f"Listener '{listener_type}' is not compatible with platform '{platform.value}'. Supported listeners: {supported}")
|
||||
logging.error(
|
||||
f"Listener '{listener_type}' is not compatible with platform '{platform.value}'. Supported listeners: {supported}"
|
||||
)
|
||||
return
|
||||
|
||||
# Initialize universal trader with platform-specific configuration
|
||||
@@ -68,39 +74,31 @@ async def start_bot(config_path: str):
|
||||
rpc_endpoint=cfg["rpc_endpoint"],
|
||||
wss_endpoint=cfg["wss_endpoint"],
|
||||
private_key=cfg["private_key"],
|
||||
|
||||
# Platform configuration - pass platform enum directly
|
||||
platform=platform,
|
||||
|
||||
# Trade parameters
|
||||
buy_amount=cfg["trade"]["buy_amount"],
|
||||
buy_slippage=cfg["trade"]["buy_slippage"],
|
||||
sell_slippage=cfg["trade"]["sell_slippage"],
|
||||
|
||||
# Extreme fast mode settings
|
||||
extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False),
|
||||
extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30),
|
||||
|
||||
# Exit strategy configuration
|
||||
exit_strategy=cfg["trade"].get("exit_strategy", "time_based"),
|
||||
take_profit_percentage=cfg["trade"].get("take_profit_percentage"),
|
||||
stop_loss_percentage=cfg["trade"].get("stop_loss_percentage"),
|
||||
max_hold_time=cfg["trade"].get("max_hold_time"),
|
||||
price_check_interval=cfg["trade"].get("price_check_interval", 10),
|
||||
|
||||
# Listener configuration
|
||||
listener_type=cfg["filters"]["listener_type"],
|
||||
|
||||
# Geyser configuration (if applicable)
|
||||
geyser_endpoint=cfg.get("geyser", {}).get("endpoint"),
|
||||
geyser_api_token=cfg.get("geyser", {}).get("api_token"),
|
||||
geyser_auth_type=cfg.get("geyser", {}).get("auth_type", "x-token"),
|
||||
|
||||
# PumpPortal configuration (if applicable)
|
||||
pumpportal_url=cfg.get("pumpportal", {}).get(
|
||||
"url", "wss://pumpportal.fun/api/data"
|
||||
),
|
||||
|
||||
# Priority fee configuration
|
||||
enable_dynamic_priority_fee=cfg.get("priority_fees", {}).get(
|
||||
"enable_dynamic", False
|
||||
@@ -109,19 +107,21 @@ async def start_bot(config_path: str):
|
||||
"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),
|
||||
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_creation=cfg.get("retries", {}).get(
|
||||
"wait_after_creation", 15
|
||||
),
|
||||
wait_time_after_buy=cfg.get("retries", {}).get("wait_after_buy", 15),
|
||||
wait_time_before_new_token=cfg.get("retries", {}).get(
|
||||
"wait_before_new_token", 15
|
||||
),
|
||||
max_token_age=cfg.get("filters", {}).get("max_token_age", 0.001),
|
||||
token_wait_timeout=cfg.get("timing", {}).get("token_wait_timeout", 120),
|
||||
|
||||
# Cleanup settings
|
||||
cleanup_mode=cfg.get("cleanup", {}).get("mode", "disabled"),
|
||||
cleanup_force_close_with_burn=cfg.get("cleanup", {}).get(
|
||||
@@ -130,7 +130,6 @@ async def start_bot(config_path: str):
|
||||
cleanup_with_priority_fee=cfg.get("cleanup", {}).get(
|
||||
"with_priority_fee", False
|
||||
),
|
||||
|
||||
# Trading filters
|
||||
match_string=cfg["filters"].get("match_string"),
|
||||
bro_address=cfg["filters"].get("bro_address"),
|
||||
@@ -139,7 +138,7 @@ async def start_bot(config_path: str):
|
||||
)
|
||||
|
||||
await trader.start()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(f"Failed to initialize or start trader: {e}")
|
||||
raise
|
||||
@@ -180,40 +179,52 @@ def run_all_bots():
|
||||
# Validate platform configuration
|
||||
try:
|
||||
platform = get_platform_from_config(cfg)
|
||||
|
||||
|
||||
# Check platform support
|
||||
from platforms import platform_factory
|
||||
|
||||
if not platform_factory.registry.is_platform_supported(platform):
|
||||
logging.error(f"Platform {platform.value} is not supported for bot '{bot_name}'. Available platforms: {[p.value for p in platform_factory.get_supported_platforms()]}")
|
||||
logging.error(
|
||||
f"Platform {platform.value} is not supported for bot '{bot_name}'. Available platforms: {[p.value for p in platform_factory.get_supported_platforms()]}"
|
||||
)
|
||||
skipped_bots += 1
|
||||
continue
|
||||
|
||||
|
||||
# Validate listener compatibility
|
||||
listener_type = cfg["filters"]["listener_type"]
|
||||
if not validate_platform_listener_combination(platform, listener_type):
|
||||
from config_loader import get_supported_listeners_for_platform
|
||||
|
||||
supported = get_supported_listeners_for_platform(platform)
|
||||
logging.error(f"Listener '{listener_type}' is not compatible with platform '{platform.value}' for bot '{bot_name}'. Supported listeners: {supported}")
|
||||
logging.error(
|
||||
f"Listener '{listener_type}' is not compatible with platform '{platform.value}' for bot '{bot_name}'. Supported listeners: {supported}"
|
||||
)
|
||||
skipped_bots += 1
|
||||
continue
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(f"Invalid platform configuration for bot '{bot_name}': {e}. Skipping...")
|
||||
logging.exception(
|
||||
f"Invalid platform configuration for bot '{bot_name}': {e}. Skipping..."
|
||||
)
|
||||
skipped_bots += 1
|
||||
continue
|
||||
|
||||
# Start bot in separate process or main process
|
||||
if cfg.get("separate_process", False):
|
||||
logging.info(f"Starting bot '{bot_name}' ({platform.value}) in separate process")
|
||||
logging.info(
|
||||
f"Starting bot '{bot_name}' ({platform.value}) in separate process"
|
||||
)
|
||||
p = multiprocessing.Process(
|
||||
target=run_bot_process, args=(str(file),), name=f"bot-{bot_name}"
|
||||
)
|
||||
p.start()
|
||||
processes.append(p)
|
||||
else:
|
||||
logging.info(f"Starting bot '{bot_name}' ({platform.value}) in main process")
|
||||
logging.info(
|
||||
f"Starting bot '{bot_name}' ({platform.value}) in main process"
|
||||
)
|
||||
asyncio.run(start_bot(str(file)))
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.exception(f"Failed to start bot from {file}: {e}")
|
||||
skipped_bots += 1
|
||||
@@ -237,15 +248,17 @@ def main() -> None:
|
||||
# Log supported platforms and listeners
|
||||
try:
|
||||
from platforms import platform_factory
|
||||
|
||||
supported_platforms = platform_factory.get_supported_platforms()
|
||||
logging.info(f"Supported platforms: {[p.value for p in supported_platforms]}")
|
||||
|
||||
|
||||
# Log listener compatibility for each platform
|
||||
from config_loader import get_supported_listeners_for_platform
|
||||
|
||||
for platform in supported_platforms:
|
||||
listeners = get_supported_listeners_for_platform(platform)
|
||||
logging.info(f"Platform {platform.value} supports listeners: {listeners}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"Could not load platform information: {e}")
|
||||
|
||||
@@ -253,4 +266,4 @@ def main() -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
+120
-57
@@ -25,14 +25,50 @@ REQUIRED_FIELDS = [
|
||||
]
|
||||
|
||||
CONFIG_VALIDATION_RULES = [
|
||||
("trade.buy_amount", (int, float), 0, float("inf"), "trade.buy_amount must be a positive number"),
|
||||
(
|
||||
"trade.buy_amount",
|
||||
(int, float),
|
||||
0,
|
||||
float("inf"),
|
||||
"trade.buy_amount must be a positive number",
|
||||
),
|
||||
("trade.buy_slippage", float, 0, 1, "trade.buy_slippage must be between 0 and 1"),
|
||||
("trade.sell_slippage", float, 0, 1, "trade.sell_slippage must be between 0 and 1"),
|
||||
("priority_fees.fixed_amount", int, 0, float("inf"), "priority_fees.fixed_amount must be a non-negative integer"),
|
||||
("priority_fees.extra_percentage", float, 0, 1, "priority_fees.extra_percentage must be between 0 and 1"),
|
||||
("priority_fees.hard_cap", int, 0, float("inf"), "priority_fees.hard_cap must be a non-negative integer"),
|
||||
("retries.max_attempts", int, 0, 100, "retries.max_attempts must be between 0 and 100"),
|
||||
("filters.max_token_age", (int, float), 0, float("inf"), "filters.max_token_age must be a non-negative number"),
|
||||
(
|
||||
"priority_fees.fixed_amount",
|
||||
int,
|
||||
0,
|
||||
float("inf"),
|
||||
"priority_fees.fixed_amount must be a non-negative integer",
|
||||
),
|
||||
(
|
||||
"priority_fees.extra_percentage",
|
||||
float,
|
||||
0,
|
||||
1,
|
||||
"priority_fees.extra_percentage must be between 0 and 1",
|
||||
),
|
||||
(
|
||||
"priority_fees.hard_cap",
|
||||
int,
|
||||
0,
|
||||
float("inf"),
|
||||
"priority_fees.hard_cap must be a non-negative integer",
|
||||
),
|
||||
(
|
||||
"retries.max_attempts",
|
||||
int,
|
||||
0,
|
||||
100,
|
||||
"retries.max_attempts must be between 0 and 100",
|
||||
),
|
||||
(
|
||||
"filters.max_token_age",
|
||||
(int, float),
|
||||
0,
|
||||
float("inf"),
|
||||
"filters.max_token_age must be a non-negative number",
|
||||
),
|
||||
]
|
||||
|
||||
# Valid values for enum-like fields
|
||||
@@ -66,17 +102,18 @@ def load_bot_config(path: str) -> dict:
|
||||
load_dotenv(env_file, override=True)
|
||||
|
||||
resolve_env_vars(config)
|
||||
|
||||
|
||||
# Set default platform if not specified (backward compatibility)
|
||||
if "platform" not in config:
|
||||
config["platform"] = "pump_fun"
|
||||
|
||||
|
||||
validate_config(config)
|
||||
return config
|
||||
|
||||
|
||||
def resolve_env_vars(config: dict) -> None:
|
||||
"""Recursively resolve environment variables in the configuration."""
|
||||
|
||||
def resolve_env(value):
|
||||
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
|
||||
env_var = value[2:-1]
|
||||
@@ -144,7 +181,9 @@ def validate_config(config: dict) -> None:
|
||||
dynamic = get_nested_value(config, "priority_fees.enable_dynamic")
|
||||
fixed = get_nested_value(config, "priority_fees.enable_fixed")
|
||||
if dynamic and fixed:
|
||||
raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously")
|
||||
raise ValueError(
|
||||
"Cannot enable both dynamic and fixed priority fees simultaneously"
|
||||
)
|
||||
except ValueError as e:
|
||||
if "Missing required config key" not in str(e):
|
||||
raise
|
||||
@@ -156,7 +195,9 @@ def validate_config(config: dict) -> None:
|
||||
validate_platform_config(config, platform)
|
||||
except ValueError as e:
|
||||
if "is not a valid" in str(e):
|
||||
raise ValueError(f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}")
|
||||
raise ValueError(
|
||||
f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}"
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
@@ -165,8 +206,11 @@ def validate_platform_config(config: dict, platform: Platform) -> None:
|
||||
# Check if platform is supported
|
||||
try:
|
||||
from platforms import platform_factory
|
||||
|
||||
if not platform_factory.registry.is_platform_supported(platform):
|
||||
raise ValueError(f"Platform {platform.value} is not supported. Available platforms: {[p.value for p in platform_factory.get_supported_platforms()]}")
|
||||
raise ValueError(
|
||||
f"Platform {platform.value} is not supported. Available platforms: {[p.value for p in platform_factory.get_supported_platforms()]}"
|
||||
)
|
||||
except ImportError:
|
||||
# If platform factory not available, just validate enum
|
||||
pass
|
||||
@@ -175,7 +219,7 @@ def validate_platform_config(config: dict, platform: Platform) -> None:
|
||||
try:
|
||||
listener_type = get_nested_value(config, "filters.listener_type")
|
||||
compatible_listeners = PLATFORM_LISTENER_COMPATIBILITY.get(platform, [])
|
||||
|
||||
|
||||
if listener_type not in compatible_listeners:
|
||||
raise ValueError(
|
||||
f"Listener type '{listener_type}' is not compatible with platform '{platform.value}'. "
|
||||
@@ -189,7 +233,7 @@ def validate_platform_config(config: dict, platform: Platform) -> None:
|
||||
if platform == Platform.PUMP_FUN:
|
||||
# pump.fun doesn't require additional config beyond base requirements
|
||||
pass
|
||||
|
||||
|
||||
elif platform == Platform.LETS_BONK:
|
||||
# LetsBonk may require additional configuration in the future
|
||||
# For now, it uses the same base configuration as pump.fun
|
||||
@@ -202,16 +246,20 @@ def get_platform_from_config(config: dict) -> Platform:
|
||||
try:
|
||||
return Platform(platform_str)
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}")
|
||||
raise ValueError(
|
||||
f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}"
|
||||
)
|
||||
|
||||
|
||||
def validate_platform_listener_combination(platform: Platform, listener_type: str) -> bool:
|
||||
def validate_platform_listener_combination(
|
||||
platform: Platform, listener_type: str
|
||||
) -> bool:
|
||||
"""Check if a platform and listener type are compatible.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform enum
|
||||
listener_type: Listener type string
|
||||
|
||||
|
||||
Returns:
|
||||
True if combination is valid
|
||||
"""
|
||||
@@ -221,10 +269,10 @@ def validate_platform_listener_combination(platform: Platform, listener_type: st
|
||||
|
||||
def get_supported_listeners_for_platform(platform: Platform) -> list[str]:
|
||||
"""Get list of supported listener types for a platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform enum
|
||||
|
||||
|
||||
Returns:
|
||||
List of supported listener types
|
||||
"""
|
||||
@@ -233,10 +281,10 @@ def get_supported_listeners_for_platform(platform: Platform) -> list[str]:
|
||||
|
||||
def get_platform_specific_required_config(platform: Platform) -> list[str]:
|
||||
"""Get platform-specific required configuration paths.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform enum
|
||||
|
||||
|
||||
Returns:
|
||||
List of additional required config paths for the platform
|
||||
"""
|
||||
@@ -251,17 +299,23 @@ def get_platform_specific_required_config(platform: Platform) -> list[str]:
|
||||
def print_config_summary(config: dict) -> None:
|
||||
"""Print a summary of the loaded configuration with platform info."""
|
||||
platform_str = config.get("platform", "pump_fun")
|
||||
|
||||
|
||||
print(f"Bot name: {config.get('name', 'unnamed')}")
|
||||
print(f"Platform: {platform_str}")
|
||||
print(f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}")
|
||||
print(
|
||||
f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}"
|
||||
)
|
||||
|
||||
# Validate platform-listener combination
|
||||
try:
|
||||
platform = Platform(platform_str)
|
||||
listener_type = config.get('filters', {}).get('listener_type')
|
||||
if listener_type and not validate_platform_listener_combination(platform, listener_type):
|
||||
print(f"WARNING: Listener '{listener_type}' may not be compatible with platform '{platform_str}'")
|
||||
listener_type = config.get("filters", {}).get("listener_type")
|
||||
if listener_type and not validate_platform_listener_combination(
|
||||
platform, listener_type
|
||||
):
|
||||
print(
|
||||
f"WARNING: Listener '{listener_type}' may not be compatible with platform '{platform_str}'"
|
||||
)
|
||||
except ValueError:
|
||||
print(f"WARNING: Invalid platform '{platform_str}'")
|
||||
|
||||
@@ -269,24 +323,28 @@ def print_config_summary(config: dict) -> None:
|
||||
print("Trade settings:")
|
||||
print(f" - Buy amount: {trade.get('buy_amount', 'not configured')} SOL")
|
||||
print(f" - Buy slippage: {trade.get('buy_slippage', 'not configured') * 100}%")
|
||||
print(f" - Extreme fast mode: {'enabled' if trade.get('extreme_fast_mode') else 'disabled'}")
|
||||
print(
|
||||
f" - Extreme fast mode: {'enabled' if trade.get('extreme_fast_mode') else 'disabled'}"
|
||||
)
|
||||
|
||||
fees = config.get("priority_fees", {})
|
||||
print("Priority fees:")
|
||||
if fees.get("enable_dynamic"):
|
||||
print(" - Dynamic fees enabled")
|
||||
elif fees.get("enable_fixed"):
|
||||
print(f" - Fixed fee: {fees.get('fixed_amount', 'not configured')} microlamports")
|
||||
print(
|
||||
f" - Fixed fee: {fees.get('fixed_amount', 'not configured')} microlamports"
|
||||
)
|
||||
|
||||
print("Configuration loaded successfully!")
|
||||
|
||||
|
||||
def validate_all_platform_configs(config_dir: str = "bots") -> dict[str, Any]:
|
||||
"""Validate all bot configurations in a directory.
|
||||
|
||||
|
||||
Args:
|
||||
config_dir: Directory containing bot config files
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary with validation results
|
||||
"""
|
||||
@@ -296,50 +354,55 @@ def validate_all_platform_configs(config_dir: str = "bots") -> dict[str, Any]:
|
||||
"platform_distribution": {},
|
||||
"listener_distribution": {},
|
||||
}
|
||||
|
||||
|
||||
config_files = list(Path(config_dir).glob("*.yaml"))
|
||||
|
||||
|
||||
for config_file in config_files:
|
||||
try:
|
||||
config = load_bot_config(config_file)
|
||||
platform = get_platform_from_config(config)
|
||||
listener_type = config.get('filters', {}).get('listener_type', 'unknown')
|
||||
|
||||
results["valid_configs"].append({
|
||||
"file": config_file,
|
||||
"name": config.get("name"),
|
||||
"platform": platform.value,
|
||||
"listener": listener_type,
|
||||
"enabled": config.get("enabled", True)
|
||||
})
|
||||
|
||||
listener_type = config.get("filters", {}).get("listener_type", "unknown")
|
||||
|
||||
results["valid_configs"].append(
|
||||
{
|
||||
"file": config_file,
|
||||
"name": config.get("name"),
|
||||
"platform": platform.value,
|
||||
"listener": listener_type,
|
||||
"enabled": config.get("enabled", True),
|
||||
}
|
||||
)
|
||||
|
||||
# Track distributions
|
||||
platform_key = platform.value
|
||||
results["platform_distribution"][platform_key] = results["platform_distribution"].get(platform_key, 0) + 1
|
||||
results["listener_distribution"][listener_type] = results["listener_distribution"].get(listener_type, 0) + 1
|
||||
|
||||
results["platform_distribution"][platform_key] = (
|
||||
results["platform_distribution"].get(platform_key, 0) + 1
|
||||
)
|
||||
results["listener_distribution"][listener_type] = (
|
||||
results["listener_distribution"].get(listener_type, 0) + 1
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
results["invalid_configs"].append({
|
||||
"file": config_file,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
results["invalid_configs"].append({"file": config_file, "error": str(e)})
|
||||
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage with platform configuration validation
|
||||
import sys
|
||||
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
config_path = sys.argv[1]
|
||||
try:
|
||||
config = load_bot_config(config_path)
|
||||
print_config_summary(config)
|
||||
|
||||
|
||||
platform = get_platform_from_config(config)
|
||||
print(f"Detected platform: {platform}")
|
||||
print(f"Supported listeners for this platform: {get_supported_listeners_for_platform(platform)}")
|
||||
print(
|
||||
f"Supported listeners for this platform: {get_supported_listeners_for_platform(platform)}"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Configuration error: {e}")
|
||||
else:
|
||||
@@ -350,8 +413,8 @@ if __name__ == "__main__":
|
||||
print(f"Invalid configs: {len(results['invalid_configs'])}")
|
||||
print(f"Platform distribution: {results['platform_distribution']}")
|
||||
print(f"Listener distribution: {results['listener_distribution']}")
|
||||
|
||||
if results['invalid_configs']:
|
||||
|
||||
if results["invalid_configs"]:
|
||||
print("\nInvalid configurations:")
|
||||
for invalid in results['invalid_configs']:
|
||||
print(f" {invalid['file']}: {invalid['error']}")
|
||||
for invalid in results["invalid_configs"]:
|
||||
print(f" {invalid['file']}: {invalid['error']}")
|
||||
|
||||
+8
-8
@@ -14,7 +14,9 @@ TOKEN_DECIMALS: Final[int] = 6
|
||||
|
||||
# Token account constants
|
||||
TOKEN_ACCOUNT_SIZE: Final[int] = 165 # Size of a token account in bytes
|
||||
TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE: Final[int] = 2_039_280 # Rent-exempt minimum for token accounts
|
||||
TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE: Final[int] = (
|
||||
2_039_280 # Rent-exempt minimum for token accounts
|
||||
)
|
||||
|
||||
# Core system programs
|
||||
SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111")
|
||||
@@ -26,9 +28,7 @@ ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||
)
|
||||
|
||||
# System accounts
|
||||
RENT: Final[Pubkey] = Pubkey.from_string(
|
||||
"SysvarRent111111111111111111111111111111111"
|
||||
)
|
||||
RENT: Final[Pubkey] = Pubkey.from_string("SysvarRent111111111111111111111111111111111")
|
||||
|
||||
# Native SOL token
|
||||
SOL_MINT: Final[Pubkey] = Pubkey.from_string(
|
||||
@@ -38,18 +38,18 @@ SOL_MINT: Final[Pubkey] = Pubkey.from_string(
|
||||
|
||||
class SystemAddresses:
|
||||
"""System-level Solana addresses shared across all platforms."""
|
||||
|
||||
|
||||
# Reference the module-level constants
|
||||
SYSTEM_PROGRAM = SYSTEM_PROGRAM
|
||||
TOKEN_PROGRAM = TOKEN_PROGRAM
|
||||
ASSOCIATED_TOKEN_PROGRAM = ASSOCIATED_TOKEN_PROGRAM
|
||||
RENT = RENT
|
||||
SOL_MINT = SOL_MINT
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_all_system_addresses(cls) -> dict[str, Pubkey]:
|
||||
"""Get all system addresses as a dictionary.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary mapping address names to Pubkey objects
|
||||
"""
|
||||
@@ -59,4 +59,4 @@ class SystemAddresses:
|
||||
"associated_token_program": cls.ASSOCIATED_TOKEN_PROGRAM,
|
||||
"rent": cls.RENT,
|
||||
"sol_mint": cls.SOL_MINT,
|
||||
}
|
||||
}
|
||||
|
||||
+78
-91
@@ -16,6 +16,7 @@ from solders.pubkey import Pubkey
|
||||
|
||||
class Platform(Enum):
|
||||
"""Supported trading platforms."""
|
||||
|
||||
PUMP_FUN = "pump_fun"
|
||||
LETS_BONK = "lets_bonk"
|
||||
|
||||
@@ -23,12 +24,13 @@ class Platform(Enum):
|
||||
@dataclass
|
||||
class TokenInfo:
|
||||
"""Enhanced token information with platform support."""
|
||||
|
||||
# Core token data
|
||||
name: str
|
||||
symbol: str
|
||||
uri: str
|
||||
mint: Pubkey
|
||||
|
||||
|
||||
# Platform-specific fields
|
||||
platform: Platform
|
||||
bonding_curve: Pubkey | None = None # pump.fun specific
|
||||
@@ -36,12 +38,12 @@ class TokenInfo:
|
||||
pool_state: Pubkey | None = None # LetsBonk specific
|
||||
base_vault: Pubkey | None = None # LetsBonk specific
|
||||
quote_vault: Pubkey | None = None # LetsBonk specific
|
||||
|
||||
|
||||
# Common fields
|
||||
user: Pubkey | None = None
|
||||
creator: Pubkey | None = None
|
||||
creator_vault: Pubkey | None = None
|
||||
|
||||
|
||||
# Metadata
|
||||
creation_timestamp: float | None = None
|
||||
additional_data: dict[str, Any] | None = None
|
||||
@@ -49,61 +51,63 @@ class TokenInfo:
|
||||
|
||||
class AddressProvider(ABC):
|
||||
"""Abstract interface for platform-specific address management."""
|
||||
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this provider serves."""
|
||||
pass
|
||||
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def program_id(self) -> Pubkey:
|
||||
"""Get the main program ID for this platform."""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def get_system_addresses(self) -> dict[str, Pubkey]:
|
||||
"""Get all system addresses required for this platform.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary mapping address names to Pubkey objects
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def derive_pool_address(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey:
|
||||
def derive_pool_address(
|
||||
self, base_mint: Pubkey, quote_mint: Pubkey | None = None
|
||||
) -> Pubkey:
|
||||
"""Derive the pool/curve address for trading pair.
|
||||
|
||||
|
||||
Args:
|
||||
base_mint: Base token mint address
|
||||
quote_mint: Quote token mint address (if applicable)
|
||||
|
||||
|
||||
Returns:
|
||||
Pool/curve address for the trading pair
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
|
||||
"""Derive user's token account address.
|
||||
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
mint: Token mint address
|
||||
|
||||
|
||||
Returns:
|
||||
User's token account address
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
||||
"""Get platform-specific additional accounts needed for trading.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary of additional account addresses
|
||||
"""
|
||||
@@ -112,13 +116,13 @@ class AddressProvider(ABC):
|
||||
|
||||
class InstructionBuilder(ABC):
|
||||
"""Abstract interface for building platform-specific trading instructions."""
|
||||
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this builder serves."""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def build_buy_instruction(
|
||||
self,
|
||||
@@ -126,22 +130,22 @@ class InstructionBuilder(ABC):
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
address_provider: AddressProvider,
|
||||
) -> list[Instruction]:
|
||||
"""Build buy instruction(s) for the platform.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of quote tokens to spend
|
||||
minimum_amount_out: Minimum base tokens expected
|
||||
address_provider: Platform address provider
|
||||
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the buy operation
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def build_sell_instruction(
|
||||
self,
|
||||
@@ -149,55 +153,49 @@ class InstructionBuilder(ABC):
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
address_provider: AddressProvider,
|
||||
) -> list[Instruction]:
|
||||
"""Build sell instruction(s) for the platform.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of base tokens to sell
|
||||
minimum_amount_out: Minimum quote tokens expected
|
||||
address_provider: Platform address provider
|
||||
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the sell operation
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def get_required_accounts_for_buy(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for buy operation (for priority fee calculation).
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def get_required_accounts_for_sell(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for sell operation (for priority fee calculation).
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
@@ -206,78 +204,74 @@ class InstructionBuilder(ABC):
|
||||
|
||||
class CurveManager(ABC):
|
||||
"""Abstract interface for platform-specific price calculations and pool state management."""
|
||||
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this manager serves."""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get the current state of a trading pool/curve.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool/curve
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary containing pool state data
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def calculate_price(self, pool_address: Pubkey) -> float:
|
||||
"""Calculate current token price from pool state.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool/curve
|
||||
|
||||
|
||||
Returns:
|
||||
Current token price in quote token units
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def calculate_buy_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
self, pool_address: Pubkey, amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected tokens received for a buy operation.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool/curve
|
||||
amount_in: Amount of quote tokens to spend
|
||||
|
||||
|
||||
Returns:
|
||||
Expected amount of base tokens to receive
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def calculate_sell_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
self, pool_address: Pubkey, amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected quote tokens received for a sell operation.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool/curve
|
||||
amount_in: Amount of base tokens to sell
|
||||
|
||||
|
||||
Returns:
|
||||
Expected amount of quote tokens to receive
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]:
|
||||
"""Get current pool reserves.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool/curve
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (base_reserves, quote_reserves)
|
||||
"""
|
||||
@@ -286,93 +280,86 @@ class CurveManager(ABC):
|
||||
|
||||
class EventParser(ABC):
|
||||
"""Abstract interface for parsing platform-specific token creation events."""
|
||||
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this parser serves."""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def parse_token_creation_from_logs(
|
||||
self,
|
||||
logs: list[str],
|
||||
signature: str
|
||||
self, logs: list[str], signature: str
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from transaction logs.
|
||||
|
||||
|
||||
Args:
|
||||
logs: List of log strings from transaction
|
||||
signature: Transaction signature
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def parse_token_creation_from_instruction(
|
||||
self,
|
||||
instruction_data: bytes,
|
||||
accounts: list[int],
|
||||
account_keys: list[bytes]
|
||||
self, instruction_data: bytes, accounts: list[int], account_keys: list[bytes]
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from instruction data.
|
||||
|
||||
|
||||
Args:
|
||||
instruction_data: Raw instruction data
|
||||
accounts: List of account indices
|
||||
account_keys: List of account public keys
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def parse_token_creation_from_geyser(
|
||||
self,
|
||||
transaction_info: Any
|
||||
self, transaction_info: Any
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from Geyser transaction data.
|
||||
|
||||
|
||||
Args:
|
||||
transaction_info: Geyser transaction information
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def parse_token_creation_from_block(
|
||||
self,
|
||||
block_data: dict[str, Any]
|
||||
self, block_data: dict[str, Any]
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from block data.
|
||||
|
||||
|
||||
Args:
|
||||
block_data: Block data containing transactions
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def get_program_id(self) -> Pubkey:
|
||||
"""Get the program ID this parser monitors.
|
||||
|
||||
|
||||
Returns:
|
||||
Program ID for event filtering
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@abstractmethod
|
||||
def get_instruction_discriminators(self) -> list[bytes]:
|
||||
"""Get instruction discriminators for token creation.
|
||||
|
||||
|
||||
Returns:
|
||||
List of discriminator bytes to match
|
||||
"""
|
||||
pass
|
||||
pass
|
||||
|
||||
@@ -13,7 +13,7 @@ class BaseTokenListener(ABC):
|
||||
|
||||
def __init__(self, platform: Platform | None = None):
|
||||
"""Initialize the listener with optional platform specification.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to monitor (if None, monitor all platforms)
|
||||
"""
|
||||
@@ -38,13 +38,13 @@ class BaseTokenListener(ABC):
|
||||
|
||||
def should_process_token(self, token_info: TokenInfo) -> bool:
|
||||
"""Check if a token should be processed based on platform filter.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
|
||||
|
||||
Returns:
|
||||
True if token should be processed
|
||||
"""
|
||||
if self.platform is None:
|
||||
return True # Process all platforms
|
||||
return token_info.platform == self.platform
|
||||
return token_info.platform == self.platform
|
||||
|
||||
@@ -48,7 +48,7 @@ class ListenerFactory:
|
||||
)
|
||||
|
||||
from monitoring.universal_geyser_listener import UniversalGeyserListener
|
||||
|
||||
|
||||
listener = UniversalGeyserListener(
|
||||
geyser_endpoint=geyser_endpoint,
|
||||
geyser_api_token=geyser_api_token,
|
||||
@@ -63,7 +63,7 @@ class ListenerFactory:
|
||||
raise ValueError("WebSocket endpoint is required for logs listener")
|
||||
|
||||
from monitoring.universal_logs_listener import UniversalLogsListener
|
||||
|
||||
|
||||
listener = UniversalLogsListener(
|
||||
wss_endpoint=wss_endpoint,
|
||||
platforms=platforms,
|
||||
@@ -76,7 +76,7 @@ class ListenerFactory:
|
||||
raise ValueError("WebSocket endpoint is required for blocks listener")
|
||||
|
||||
from monitoring.universal_block_listener import UniversalBlockListener
|
||||
|
||||
|
||||
listener = UniversalBlockListener(
|
||||
wss_endpoint=wss_endpoint,
|
||||
platforms=platforms,
|
||||
@@ -89,26 +89,36 @@ class ListenerFactory:
|
||||
from monitoring.universal_pumpportal_listener import (
|
||||
UniversalPumpPortalListener,
|
||||
)
|
||||
|
||||
|
||||
# Validate that requested platforms support PumpPortal
|
||||
supported_pumpportal_platforms = [Platform.PUMP_FUN, Platform.LETS_BONK]
|
||||
|
||||
|
||||
if platforms:
|
||||
unsupported = [p for p in platforms if p not in supported_pumpportal_platforms]
|
||||
unsupported = [
|
||||
p for p in platforms if p not in supported_pumpportal_platforms
|
||||
]
|
||||
if unsupported:
|
||||
logger.warning(f"Platforms {[p.value for p in unsupported]} do not support PumpPortal")
|
||||
|
||||
logger.warning(
|
||||
f"Platforms {[p.value for p in unsupported]} do not support PumpPortal"
|
||||
)
|
||||
|
||||
# Filter to only supported platforms
|
||||
filtered_platforms = [p for p in platforms if p in supported_pumpportal_platforms]
|
||||
filtered_platforms = [
|
||||
p for p in platforms if p in supported_pumpportal_platforms
|
||||
]
|
||||
if not filtered_platforms:
|
||||
raise ValueError("No supported platforms specified for PumpPortal listener")
|
||||
raise ValueError(
|
||||
"No supported platforms specified for PumpPortal listener"
|
||||
)
|
||||
platforms = filtered_platforms
|
||||
|
||||
|
||||
listener = UniversalPumpPortalListener(
|
||||
pumpportal_url=pumpportal_url,
|
||||
platforms=platforms,
|
||||
)
|
||||
logger.info(f"Created Universal PumpPortal listener for platforms: {[p.value for p in (platforms or supported_pumpportal_platforms)]}")
|
||||
logger.info(
|
||||
f"Created Universal PumpPortal listener for platforms: {[p.value for p in (platforms or supported_pumpportal_platforms)]}"
|
||||
)
|
||||
return listener
|
||||
|
||||
else:
|
||||
@@ -146,8 +156,8 @@ class ListenerFactory:
|
||||
@staticmethod
|
||||
def get_pumpportal_supported_platforms() -> list[Platform]:
|
||||
"""Get list of platforms that support PumpPortal listener.
|
||||
|
||||
|
||||
Returns:
|
||||
List of platforms with PumpPortal support
|
||||
"""
|
||||
return [Platform.PUMP_FUN, Platform.LETS_BONK]
|
||||
return [Platform.PUMP_FUN, Platform.LETS_BONK]
|
||||
|
||||
@@ -33,25 +33,25 @@ class UniversalBlockListener(BaseTokenListener):
|
||||
super().__init__()
|
||||
self.wss_endpoint = wss_endpoint
|
||||
self.ping_interval = 20 # seconds
|
||||
|
||||
|
||||
# Import platform factory and get supported platforms
|
||||
from platforms import platform_factory
|
||||
|
||||
|
||||
if platforms is None:
|
||||
# Monitor all supported platforms
|
||||
self.platforms = platform_factory.get_supported_platforms()
|
||||
else:
|
||||
self.platforms = platforms
|
||||
|
||||
|
||||
# Get event parsers for all platforms
|
||||
self.platform_parsers = {}
|
||||
self.platform_program_ids = []
|
||||
|
||||
|
||||
for platform in self.platforms:
|
||||
try:
|
||||
# Create a simple dummy client that doesn't start blockhash updater
|
||||
from core.client import SolanaClient
|
||||
|
||||
|
||||
# Create a mock client class to avoid network operations
|
||||
class DummyClient(SolanaClient):
|
||||
def __init__(self):
|
||||
@@ -61,16 +61,18 @@ class UniversalBlockListener(BaseTokenListener):
|
||||
self._cached_blockhash = None
|
||||
self._blockhash_lock = None
|
||||
self._blockhash_updater_task = None
|
||||
|
||||
|
||||
dummy_client = DummyClient()
|
||||
|
||||
|
||||
implementations = get_platform_implementations(platform, dummy_client)
|
||||
parser = implementations.event_parser
|
||||
self.platform_parsers[platform] = parser
|
||||
self.platform_program_ids.append(str(parser.get_program_id()))
|
||||
|
||||
logger.info(f"Registered platform {platform.value} with program ID {parser.get_program_id()}")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"Registered platform {platform.value} with program ID {parser.get_program_id()}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not register platform {platform.value}: {e}")
|
||||
|
||||
@@ -117,7 +119,10 @@ class UniversalBlockListener(BaseTokenListener):
|
||||
)
|
||||
continue
|
||||
|
||||
if creator_address and str(token_info.user) != creator_address:
|
||||
if (
|
||||
creator_address
|
||||
and str(token_info.user) != creator_address
|
||||
):
|
||||
logger.info(
|
||||
f"Token not created by {creator_address}. Skipping..."
|
||||
)
|
||||
@@ -220,10 +225,10 @@ class UniversalBlockListener(BaseTokenListener):
|
||||
|
||||
for platform, parser in self.platform_parsers.items():
|
||||
# Check if the parser has a block parsing method
|
||||
if hasattr(parser, 'parse_token_creation_from_block'):
|
||||
token_info = parser.parse_token_creation_from_block({
|
||||
"transactions": [tx]
|
||||
})
|
||||
if hasattr(parser, "parse_token_creation_from_block"):
|
||||
token_info = parser.parse_token_creation_from_block(
|
||||
{"transactions": [tx]}
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
@@ -237,4 +242,4 @@ class UniversalBlockListener(BaseTokenListener):
|
||||
except Exception:
|
||||
logger.exception("Error processing WebSocket message")
|
||||
|
||||
return None
|
||||
return None
|
||||
|
||||
@@ -30,7 +30,7 @@ class UniversalGeyserListener(BaseTokenListener):
|
||||
super().__init__()
|
||||
self.geyser_endpoint = geyser_endpoint
|
||||
self.geyser_api_token = geyser_api_token
|
||||
|
||||
|
||||
valid_auth_types = {"x-token", "basic"}
|
||||
self.auth_type: str = (geyser_auth_type or "x-token").lower()
|
||||
if self.auth_type not in valid_auth_types:
|
||||
@@ -38,21 +38,21 @@ class UniversalGeyserListener(BaseTokenListener):
|
||||
f"Unsupported auth_type={self.auth_type!r}. "
|
||||
f"Expected one of {valid_auth_types}"
|
||||
)
|
||||
|
||||
|
||||
if platforms is None:
|
||||
self.platforms = platform_factory.get_supported_platforms()
|
||||
else:
|
||||
self.platforms = platforms
|
||||
|
||||
|
||||
# Get event parsers for all platforms
|
||||
self.platform_parsers = {}
|
||||
self.platform_program_ids = set()
|
||||
|
||||
|
||||
for platform in self.platforms:
|
||||
try:
|
||||
# Create a simple dummy client that doesn't start blockhash updater
|
||||
from core.client import SolanaClient
|
||||
|
||||
|
||||
# Create a mock client class to avoid network operations
|
||||
class DummyClient(SolanaClient):
|
||||
def __init__(self):
|
||||
@@ -62,22 +62,26 @@ class UniversalGeyserListener(BaseTokenListener):
|
||||
self._cached_blockhash = None
|
||||
self._blockhash_lock = None
|
||||
self._blockhash_updater_task = None
|
||||
|
||||
|
||||
dummy_client = DummyClient()
|
||||
|
||||
implementations = platform_factory.create_for_platform(platform, dummy_client)
|
||||
|
||||
implementations = platform_factory.create_for_platform(
|
||||
platform, dummy_client
|
||||
)
|
||||
parser = implementations.event_parser
|
||||
self.platform_parsers[platform] = parser
|
||||
self.platform_program_ids.add(parser.get_program_id())
|
||||
|
||||
logger.info(f"Registered platform {platform.value} with program ID {parser.get_program_id()}")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"Registered platform {platform.value} with program ID {parser.get_program_id()}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not register platform {platform.value}: {e}")
|
||||
|
||||
async def _create_geyser_connection(self):
|
||||
"""Establish a secure connection to the Geyser endpoint."""
|
||||
|
||||
|
||||
if self.auth_type == "x-token":
|
||||
auth = grpc.metadata_call_credentials(
|
||||
lambda _, callback: callback(
|
||||
@@ -92,20 +96,20 @@ class UniversalGeyserListener(BaseTokenListener):
|
||||
)
|
||||
creds = grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth)
|
||||
channel = grpc.aio.secure_channel(self.geyser_endpoint, creds)
|
||||
|
||||
|
||||
return geyser_pb2_grpc.GeyserStub(channel), channel
|
||||
|
||||
def _create_subscription_request(self):
|
||||
"""Create a subscription request for all monitored platforms."""
|
||||
|
||||
|
||||
request = geyser_pb2.SubscribeRequest()
|
||||
|
||||
|
||||
# Add all platform program IDs to the filter
|
||||
for program_id in self.platform_program_ids:
|
||||
filter_name = f"platform_filter_{program_id}"
|
||||
request.transactions[filter_name].account_include.append(str(program_id))
|
||||
request.transactions[filter_name].failed = False
|
||||
|
||||
|
||||
request.commitment = geyser_pb2.CommitmentLevel.PROCESSED
|
||||
return request
|
||||
|
||||
@@ -126,8 +130,12 @@ class UniversalGeyserListener(BaseTokenListener):
|
||||
request = self._create_subscription_request()
|
||||
|
||||
logger.info(f"Connected to Geyser endpoint: {self.geyser_endpoint}")
|
||||
logger.info(f"Monitoring platforms: {[p.value for p in self.platforms]}")
|
||||
logger.info(f"Monitoring program IDs: {[str(pid) for pid in self.platform_program_ids]}")
|
||||
logger.info(
|
||||
f"Monitoring platforms: {[p.value for p in self.platforms]}"
|
||||
)
|
||||
logger.info(
|
||||
f"Monitoring program IDs: {[str(pid) for pid in self.platform_program_ids]}"
|
||||
)
|
||||
|
||||
try:
|
||||
async for update in stub.Subscribe(iter([request])):
|
||||
@@ -192,7 +200,7 @@ class UniversalGeyserListener(BaseTokenListener):
|
||||
continue
|
||||
|
||||
program_id = Pubkey.from_bytes(msg.account_keys[program_idx])
|
||||
|
||||
|
||||
# Find the matching platform parser
|
||||
for platform, parser in self.platform_parsers.items():
|
||||
if program_id == parser.get_program_id():
|
||||
@@ -207,4 +215,4 @@ class UniversalGeyserListener(BaseTokenListener):
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error processing Geyser update")
|
||||
return None
|
||||
return None
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Universal logs listener that works with any platform through the interface system.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from collections.abc import Awaitable, Callable
|
||||
@@ -31,25 +32,25 @@ class UniversalLogsListener(BaseTokenListener):
|
||||
super().__init__()
|
||||
self.wss_endpoint = wss_endpoint
|
||||
self.ping_interval = 20 # seconds
|
||||
|
||||
|
||||
# Import platform factory and get supported platforms
|
||||
from platforms import platform_factory
|
||||
|
||||
|
||||
if platforms is None:
|
||||
# Monitor all supported platforms
|
||||
self.platforms = platform_factory.get_supported_platforms()
|
||||
else:
|
||||
self.platforms = platforms
|
||||
|
||||
|
||||
# Get event parsers for all platforms
|
||||
self.platform_parsers = {}
|
||||
self.platform_program_ids = []
|
||||
|
||||
|
||||
for platform in self.platforms:
|
||||
try:
|
||||
# Create a simple dummy client that doesn't start blockhash updater
|
||||
from core.client import SolanaClient
|
||||
|
||||
|
||||
# Create a mock client class to avoid network operations
|
||||
class DummyClient(SolanaClient):
|
||||
def __init__(self):
|
||||
@@ -59,16 +60,20 @@ class UniversalLogsListener(BaseTokenListener):
|
||||
self._cached_blockhash = None
|
||||
self._blockhash_lock = None
|
||||
self._blockhash_updater_task = None
|
||||
|
||||
|
||||
dummy_client = DummyClient()
|
||||
|
||||
implementations = platform_factory.create_for_platform(platform, dummy_client)
|
||||
|
||||
implementations = platform_factory.create_for_platform(
|
||||
platform, dummy_client
|
||||
)
|
||||
parser = implementations.event_parser
|
||||
self.platform_parsers[platform] = parser
|
||||
self.platform_program_ids.append(str(parser.get_program_id()))
|
||||
|
||||
logger.info(f"Registered platform {platform.value} with program ID {parser.get_program_id()}")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"Registered platform {platform.value} with program ID {parser.get_program_id()}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not register platform {platform.value}: {e}")
|
||||
|
||||
@@ -115,7 +120,10 @@ class UniversalLogsListener(BaseTokenListener):
|
||||
)
|
||||
continue
|
||||
|
||||
if creator_address and str(token_info.user) != creator_address:
|
||||
if (
|
||||
creator_address
|
||||
and str(token_info.user) != creator_address
|
||||
):
|
||||
logger.info(
|
||||
f"Token not created by {creator_address}. Skipping..."
|
||||
)
|
||||
@@ -159,7 +167,9 @@ class UniversalLogsListener(BaseTokenListener):
|
||||
response = await websocket.recv()
|
||||
response_data = json.loads(response)
|
||||
if "result" in response_data:
|
||||
logger.info(f"Subscription confirmed with ID: {response_data['result']}")
|
||||
logger.info(
|
||||
f"Subscription confirmed with ID: {response_data['result']}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"Unexpected subscription response: {response}")
|
||||
|
||||
@@ -209,4 +219,4 @@ class UniversalLogsListener(BaseTokenListener):
|
||||
except Exception:
|
||||
logger.exception("Error processing WebSocket message")
|
||||
|
||||
return None
|
||||
return None
|
||||
|
||||
@@ -32,23 +32,23 @@ class UniversalPumpPortalListener(BaseTokenListener):
|
||||
super().__init__()
|
||||
self.pumpportal_url = pumpportal_url
|
||||
self.ping_interval = 20 # seconds
|
||||
|
||||
|
||||
# Get platform-specific processors
|
||||
from platforms.letsbonk.pumpportal_processor import LetsBonkPumpPortalProcessor
|
||||
from platforms.pumpfun.pumpportal_processor import PumpFunPumpPortalProcessor
|
||||
|
||||
|
||||
# Create processor instances
|
||||
all_processors = [
|
||||
PumpFunPumpPortalProcessor(),
|
||||
LetsBonkPumpPortalProcessor(),
|
||||
]
|
||||
|
||||
|
||||
# Filter processors based on requested platforms
|
||||
if platforms is None:
|
||||
self.processors = all_processors
|
||||
else:
|
||||
self.processors = [p for p in all_processors if p.platform in platforms]
|
||||
|
||||
|
||||
# Build mapping of pool names to processors for quick lookup
|
||||
self.pool_to_processors: dict[str, list] = {}
|
||||
for processor in self.processors:
|
||||
@@ -56,8 +56,10 @@ class UniversalPumpPortalListener(BaseTokenListener):
|
||||
if pool_name not in self.pool_to_processors:
|
||||
self.pool_to_processors[pool_name] = []
|
||||
self.pool_to_processors[pool_name].append(processor)
|
||||
|
||||
logger.info(f"Initialized Universal PumpPortal listener for platforms: {[p.platform.value for p in self.processors]}")
|
||||
|
||||
logger.info(
|
||||
f"Initialized Universal PumpPortal listener for platforms: {[p.platform.value for p in self.processors]}"
|
||||
)
|
||||
logger.info(f"Monitoring pools: {list(self.pool_to_processors.keys())}")
|
||||
|
||||
async def listen_for_tokens(
|
||||
@@ -100,8 +102,14 @@ class UniversalPumpPortalListener(BaseTokenListener):
|
||||
continue
|
||||
|
||||
if creator_address:
|
||||
creator_str = str(token_info.creator) if token_info.creator else ""
|
||||
user_str = str(token_info.user) if token_info.user else ""
|
||||
creator_str = (
|
||||
str(token_info.creator)
|
||||
if token_info.creator
|
||||
else ""
|
||||
)
|
||||
user_str = (
|
||||
str(token_info.user) if token_info.user else ""
|
||||
)
|
||||
if creator_address not in [creator_str, user_str]:
|
||||
logger.info(
|
||||
f"Token not created by {creator_address}. Skipping..."
|
||||
@@ -197,7 +205,9 @@ class UniversalPumpPortalListener(BaseTokenListener):
|
||||
if processor.can_process(token_data):
|
||||
token_info = processor.process_token_data(token_data)
|
||||
if token_info:
|
||||
logger.debug(f"Successfully processed token using {processor.platform.value} processor")
|
||||
logger.debug(
|
||||
f"Successfully processed token using {processor.platform.value} processor"
|
||||
)
|
||||
return token_info
|
||||
|
||||
logger.debug(f"No processor could handle token data from pool {pool_name}")
|
||||
@@ -213,4 +223,4 @@ class UniversalPumpPortalListener(BaseTokenListener):
|
||||
except Exception:
|
||||
logger.exception("Error processing PumpPortal WebSocket message")
|
||||
|
||||
return None
|
||||
return None
|
||||
|
||||
+115
-94
@@ -25,6 +25,7 @@ logger = get_logger(__name__)
|
||||
@dataclass
|
||||
class PlatformImplementations:
|
||||
"""Container for all platform-specific implementations."""
|
||||
|
||||
address_provider: AddressProvider
|
||||
instruction_builder: InstructionBuilder
|
||||
curve_manager: CurveManager
|
||||
@@ -33,21 +34,21 @@ class PlatformImplementations:
|
||||
|
||||
class PlatformRegistry:
|
||||
"""Registry for platform implementations."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self._implementations: dict[Platform, dict[str, type]] = {}
|
||||
self._instances: dict[tuple[Platform, str], PlatformImplementations] = {}
|
||||
|
||||
|
||||
def register_platform(
|
||||
self,
|
||||
platform: Platform,
|
||||
address_provider_class: type[AddressProvider],
|
||||
instruction_builder_class: type[InstructionBuilder],
|
||||
curve_manager_class: type[CurveManager],
|
||||
event_parser_class: type[EventParser]
|
||||
event_parser_class: type[EventParser],
|
||||
) -> None:
|
||||
"""Register platform implementations.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform enum value
|
||||
address_provider_class: AddressProvider implementation class
|
||||
@@ -56,114 +57,119 @@ class PlatformRegistry:
|
||||
event_parser_class: EventParser implementation class
|
||||
"""
|
||||
self._implementations[platform] = {
|
||||
'address_provider': address_provider_class,
|
||||
'instruction_builder': instruction_builder_class,
|
||||
'curve_manager': curve_manager_class,
|
||||
'event_parser': event_parser_class
|
||||
"address_provider": address_provider_class,
|
||||
"instruction_builder": instruction_builder_class,
|
||||
"curve_manager": curve_manager_class,
|
||||
"event_parser": event_parser_class,
|
||||
}
|
||||
|
||||
|
||||
def create_platform_implementations(
|
||||
self,
|
||||
platform: Platform,
|
||||
client: SolanaClient,
|
||||
**kwargs: Any
|
||||
self, platform: Platform, client: SolanaClient, **kwargs: Any
|
||||
) -> PlatformImplementations:
|
||||
"""Create platform implementation instances with IDL support.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to create implementations for
|
||||
client: Solana RPC client
|
||||
**kwargs: Additional arguments for implementation constructors
|
||||
|
||||
|
||||
Returns:
|
||||
PlatformImplementations containing all interface implementations
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If platform is not registered
|
||||
"""
|
||||
if platform not in self._implementations:
|
||||
raise ValueError(f"Platform {platform} is not registered")
|
||||
|
||||
|
||||
# Use client address as cache key to allow multiple clients
|
||||
cache_key = (platform, str(client.rpc_endpoint))
|
||||
|
||||
|
||||
# Check if we already have instances for this platform + client combo
|
||||
if cache_key in self._instances:
|
||||
return self._instances[cache_key]
|
||||
|
||||
|
||||
impl_classes = self._implementations[platform]
|
||||
|
||||
|
||||
# Check if platform has IDL support and prepare IDL parser
|
||||
idl_parser = None
|
||||
if has_idl_support(platform):
|
||||
try:
|
||||
idl_manager = get_idl_manager()
|
||||
idl_parser = idl_manager.get_parser(platform, verbose=kwargs.get('verbose_idl', False))
|
||||
logger.info(f"IDL parser loaded for {platform.value} platform implementations")
|
||||
idl_parser = idl_manager.get_parser(
|
||||
platform, verbose=kwargs.get("verbose_idl", False)
|
||||
)
|
||||
logger.info(
|
||||
f"IDL parser loaded for {platform.value} platform implementations"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load IDL parser for {platform.value}: {e}")
|
||||
|
||||
|
||||
# Create instances - pass IDL parser to classes that need it
|
||||
address_provider = impl_classes['address_provider']()
|
||||
|
||||
address_provider = impl_classes["address_provider"]()
|
||||
|
||||
# For platforms with IDL support, pass the parser to relevant classes
|
||||
if idl_parser and platform in [Platform.LETS_BONK, Platform.PUMP_FUN]:
|
||||
instruction_builder = impl_classes['instruction_builder'](idl_parser=idl_parser)
|
||||
curve_manager = impl_classes['curve_manager'](client, idl_parser=idl_parser)
|
||||
event_parser = impl_classes['event_parser'](idl_parser=idl_parser)
|
||||
instruction_builder = impl_classes["instruction_builder"](
|
||||
idl_parser=idl_parser
|
||||
)
|
||||
curve_manager = impl_classes["curve_manager"](client, idl_parser=idl_parser)
|
||||
event_parser = impl_classes["event_parser"](idl_parser=idl_parser)
|
||||
else:
|
||||
# Fallback for platforms without IDL support
|
||||
instruction_builder = impl_classes['instruction_builder']()
|
||||
curve_manager = impl_classes['curve_manager'](client)
|
||||
event_parser = impl_classes['event_parser']()
|
||||
|
||||
instruction_builder = impl_classes["instruction_builder"]()
|
||||
curve_manager = impl_classes["curve_manager"](client)
|
||||
event_parser = impl_classes["event_parser"]()
|
||||
|
||||
implementations = PlatformImplementations(
|
||||
address_provider=address_provider,
|
||||
instruction_builder=instruction_builder,
|
||||
curve_manager=curve_manager,
|
||||
event_parser=event_parser
|
||||
event_parser=event_parser,
|
||||
)
|
||||
|
||||
|
||||
# Cache the instances
|
||||
self._instances[cache_key] = implementations
|
||||
|
||||
|
||||
return implementations
|
||||
|
||||
def get_platform_implementations(self, platform: Platform, client_endpoint: str) -> PlatformImplementations | None:
|
||||
|
||||
def get_platform_implementations(
|
||||
self, platform: Platform, client_endpoint: str
|
||||
) -> PlatformImplementations | None:
|
||||
"""Get cached platform implementations.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get implementations for
|
||||
client_endpoint: Client endpoint for cache lookup
|
||||
|
||||
|
||||
Returns:
|
||||
PlatformImplementations if available, None otherwise
|
||||
"""
|
||||
cache_key = (platform, client_endpoint)
|
||||
return self._instances.get(cache_key)
|
||||
|
||||
|
||||
def get_supported_platforms(self) -> list[Platform]:
|
||||
"""Get list of supported platforms.
|
||||
|
||||
|
||||
Returns:
|
||||
List of registered platforms
|
||||
"""
|
||||
return list(self._implementations.keys())
|
||||
|
||||
|
||||
def is_platform_supported(self, platform: Platform) -> bool:
|
||||
"""Check if a platform is supported.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to check
|
||||
|
||||
|
||||
Returns:
|
||||
True if platform is registered, False otherwise
|
||||
"""
|
||||
return platform in self._implementations
|
||||
|
||||
|
||||
def clear_implementation_cache(self, platform: Platform | None = None) -> None:
|
||||
"""Clear cached platform implementations.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Specific platform to clear, or None to clear all
|
||||
"""
|
||||
@@ -171,7 +177,9 @@ class PlatformRegistry:
|
||||
logger.info("Clearing all cached platform implementations")
|
||||
self._instances.clear()
|
||||
else:
|
||||
keys_to_remove = [key for key in self._instances.keys() if key[0] == platform]
|
||||
keys_to_remove = [
|
||||
key for key in self._instances.keys() if key[0] == platform
|
||||
]
|
||||
for key in keys_to_remove:
|
||||
del self._instances[key]
|
||||
logger.info(f"Cleared cached implementations for {platform.value}")
|
||||
@@ -179,11 +187,11 @@ class PlatformRegistry:
|
||||
|
||||
class PlatformFactory:
|
||||
"""Factory for creating platform-specific implementations with IDL support."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
self.registry = PlatformRegistry()
|
||||
self._setup_default_platforms()
|
||||
|
||||
|
||||
def _setup_default_platforms(self) -> None:
|
||||
"""Setup default platform registrations."""
|
||||
# Import and register pump.fun platform
|
||||
@@ -194,18 +202,18 @@ class PlatformFactory:
|
||||
PumpFunEventParser,
|
||||
PumpFunInstructionBuilder,
|
||||
)
|
||||
|
||||
|
||||
self.registry.register_platform(
|
||||
Platform.PUMP_FUN,
|
||||
PumpFunAddressProvider,
|
||||
PumpFunInstructionBuilder,
|
||||
PumpFunCurveManager,
|
||||
PumpFunEventParser
|
||||
PumpFunEventParser,
|
||||
)
|
||||
|
||||
|
||||
except ImportError as e:
|
||||
print(f"Warning: Could not register pump.fun platform: {e}")
|
||||
|
||||
|
||||
# Import and register LetsBonk platform
|
||||
try:
|
||||
from platforms.letsbonk import (
|
||||
@@ -214,105 +222,116 @@ class PlatformFactory:
|
||||
LetsBonkEventParser,
|
||||
LetsBonkInstructionBuilder,
|
||||
)
|
||||
|
||||
|
||||
self.registry.register_platform(
|
||||
Platform.LETS_BONK,
|
||||
LetsBonkAddressProvider,
|
||||
LetsBonkInstructionBuilder,
|
||||
LetsBonkCurveManager,
|
||||
LetsBonkEventParser
|
||||
LetsBonkEventParser,
|
||||
)
|
||||
|
||||
|
||||
except ImportError as e:
|
||||
print(f"Warning: Could not register LetsBonk platform: {e}")
|
||||
|
||||
|
||||
def create_for_platform(
|
||||
self,
|
||||
platform: Platform,
|
||||
client: SolanaClient,
|
||||
**config: Any
|
||||
self, platform: Platform, client: SolanaClient, **config: Any
|
||||
) -> PlatformImplementations:
|
||||
"""Create all implementations for a specific platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to create implementations for
|
||||
client: Solana RPC client
|
||||
**config: Platform-specific configuration (including verbose_idl)
|
||||
|
||||
|
||||
Returns:
|
||||
PlatformImplementations containing all interface implementations
|
||||
"""
|
||||
return self.registry.create_platform_implementations(platform, client, **config)
|
||||
|
||||
def get_address_provider(self, platform: Platform, client: SolanaClient) -> AddressProvider:
|
||||
|
||||
def get_address_provider(
|
||||
self, platform: Platform, client: SolanaClient
|
||||
) -> AddressProvider:
|
||||
"""Get address provider for platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get provider for
|
||||
client: Solana RPC client
|
||||
|
||||
|
||||
Returns:
|
||||
AddressProvider implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
implementations = self.registry.create_platform_implementations(
|
||||
platform, client
|
||||
)
|
||||
return implementations.address_provider
|
||||
|
||||
def get_instruction_builder(self, platform: Platform, client: SolanaClient) -> InstructionBuilder:
|
||||
|
||||
def get_instruction_builder(
|
||||
self, platform: Platform, client: SolanaClient
|
||||
) -> InstructionBuilder:
|
||||
"""Get instruction builder for platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get builder for
|
||||
client: Solana RPC client
|
||||
|
||||
|
||||
Returns:
|
||||
InstructionBuilder implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
implementations = self.registry.create_platform_implementations(
|
||||
platform, client
|
||||
)
|
||||
return implementations.instruction_builder
|
||||
|
||||
def get_curve_manager(self, platform: Platform, client: SolanaClient) -> CurveManager:
|
||||
|
||||
def get_curve_manager(
|
||||
self, platform: Platform, client: SolanaClient
|
||||
) -> CurveManager:
|
||||
"""Get curve manager for platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get manager for
|
||||
client: Solana RPC client
|
||||
|
||||
|
||||
Returns:
|
||||
CurveManager implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
implementations = self.registry.create_platform_implementations(
|
||||
platform, client
|
||||
)
|
||||
return implementations.curve_manager
|
||||
|
||||
|
||||
def get_event_parser(self, platform: Platform, client: SolanaClient) -> EventParser:
|
||||
"""Get event parser for platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get parser for
|
||||
client: Solana RPC client
|
||||
|
||||
|
||||
Returns:
|
||||
EventParser implementation
|
||||
"""
|
||||
implementations = self.registry.create_platform_implementations(platform, client)
|
||||
implementations = self.registry.create_platform_implementations(
|
||||
platform, client
|
||||
)
|
||||
return implementations.event_parser
|
||||
|
||||
|
||||
def get_supported_platforms(self) -> list[Platform]:
|
||||
"""Get list of supported platforms.
|
||||
|
||||
|
||||
Returns:
|
||||
List of supported platforms
|
||||
"""
|
||||
return self.registry.get_supported_platforms()
|
||||
|
||||
|
||||
def clear_caches(self, platform: Platform | None = None) -> None:
|
||||
"""Clear all caches for better memory management.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Specific platform to clear, or None to clear all
|
||||
"""
|
||||
# Clear implementation cache
|
||||
self.registry.clear_implementation_cache(platform)
|
||||
|
||||
|
||||
# Clear IDL parser cache
|
||||
idl_manager = get_idl_manager()
|
||||
idl_manager.clear_cache(platform)
|
||||
@@ -322,13 +341,15 @@ class PlatformFactory:
|
||||
platform_factory = PlatformFactory()
|
||||
|
||||
|
||||
def get_platform_implementations(platform: Platform, client: SolanaClient) -> PlatformImplementations:
|
||||
def get_platform_implementations(
|
||||
platform: Platform, client: SolanaClient
|
||||
) -> PlatformImplementations:
|
||||
"""Convenience function to get platform implementations.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get implementations for
|
||||
client: Solana RPC client
|
||||
|
||||
|
||||
Returns:
|
||||
PlatformImplementations containing all interface implementations
|
||||
"""
|
||||
@@ -340,10 +361,10 @@ def register_platform_implementations(
|
||||
address_provider_class: type[AddressProvider],
|
||||
instruction_builder_class: type[InstructionBuilder],
|
||||
curve_manager_class: type[CurveManager],
|
||||
event_parser_class: type[EventParser]
|
||||
event_parser_class: type[EventParser],
|
||||
) -> None:
|
||||
"""Register platform implementations with the global factory.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform enum value
|
||||
address_provider_class: AddressProvider implementation class
|
||||
@@ -356,5 +377,5 @@ def register_platform_implementations(
|
||||
address_provider_class,
|
||||
instruction_builder_class,
|
||||
curve_manager_class,
|
||||
event_parser_class
|
||||
)
|
||||
event_parser_class,
|
||||
)
|
||||
|
||||
@@ -13,9 +13,9 @@ from .pumpportal_processor import LetsBonkPumpPortalProcessor
|
||||
|
||||
# Export implementations for direct use if needed
|
||||
__all__ = [
|
||||
'LetsBonkAddressProvider',
|
||||
'LetsBonkCurveManager',
|
||||
'LetsBonkEventParser',
|
||||
'LetsBonkInstructionBuilder',
|
||||
'LetsBonkPumpPortalProcessor'
|
||||
]
|
||||
"LetsBonkAddressProvider",
|
||||
"LetsBonkCurveManager",
|
||||
"LetsBonkEventParser",
|
||||
"LetsBonkInstructionBuilder",
|
||||
"LetsBonkPumpPortalProcessor",
|
||||
]
|
||||
|
||||
@@ -18,35 +18,41 @@ from interfaces.core import AddressProvider, Platform, TokenInfo
|
||||
@dataclass
|
||||
class LetsBonkAddresses:
|
||||
"""LetsBonk (Raydium LaunchLab) program addresses."""
|
||||
|
||||
|
||||
# Raydium LaunchLab program addresses
|
||||
PROGRAM: Final[Pubkey] = Pubkey.from_string("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj")
|
||||
GLOBAL_CONFIG: Final[Pubkey] = Pubkey.from_string("6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX")
|
||||
PLATFORM_CONFIG: Final[Pubkey] = Pubkey.from_string("FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1")
|
||||
PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||
"LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj"
|
||||
)
|
||||
GLOBAL_CONFIG: Final[Pubkey] = Pubkey.from_string(
|
||||
"6s1xP3hpbAfFoNtUNF8mfHsjr2Bd97JxFJRWLbL6aHuX"
|
||||
)
|
||||
PLATFORM_CONFIG: Final[Pubkey] = Pubkey.from_string(
|
||||
"FfYek5vEz23cMkWsdJwG2oa6EphsvXSHrGpdALN4g6W1"
|
||||
)
|
||||
|
||||
|
||||
class LetsBonkAddressProvider(AddressProvider):
|
||||
"""LetsBonk (Raydium LaunchLab) implementation of AddressProvider interface."""
|
||||
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this provider serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
|
||||
@property
|
||||
def program_id(self) -> Pubkey:
|
||||
"""Get the main program ID for this platform."""
|
||||
return LetsBonkAddresses.PROGRAM
|
||||
|
||||
|
||||
def get_system_addresses(self) -> dict[str, Pubkey]:
|
||||
"""Get all system addresses required for LetsBonk.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary mapping address names to Pubkey objects
|
||||
"""
|
||||
# Get system addresses from the single source of truth
|
||||
system_addresses = SystemAddresses.get_all_system_addresses()
|
||||
|
||||
|
||||
# Add LetsBonk specific addresses
|
||||
letsbonk_addresses = {
|
||||
# Raydium LaunchLab specific addresses
|
||||
@@ -54,179 +60,184 @@ class LetsBonkAddressProvider(AddressProvider):
|
||||
"global_config": LetsBonkAddresses.GLOBAL_CONFIG,
|
||||
"platform_config": LetsBonkAddresses.PLATFORM_CONFIG,
|
||||
}
|
||||
|
||||
|
||||
# Combine system and platform-specific addresses
|
||||
return {**system_addresses, **letsbonk_addresses}
|
||||
|
||||
def derive_pool_address(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey:
|
||||
|
||||
def derive_pool_address(
|
||||
self, base_mint: Pubkey, quote_mint: Pubkey | None = None
|
||||
) -> Pubkey:
|
||||
"""Derive the pool state address for a token pair.
|
||||
|
||||
|
||||
For LetsBonk, this derives the pool state PDA using base_mint and WSOL.
|
||||
|
||||
|
||||
Args:
|
||||
base_mint: Base token mint address
|
||||
quote_mint: Quote token mint (defaults to WSOL)
|
||||
|
||||
|
||||
Returns:
|
||||
Pool state address
|
||||
"""
|
||||
if quote_mint is None:
|
||||
quote_mint = SystemAddresses.SOL_MINT
|
||||
|
||||
|
||||
pool_state, _ = Pubkey.find_program_address(
|
||||
[b"pool", bytes(base_mint), bytes(quote_mint)],
|
||||
LetsBonkAddresses.PROGRAM
|
||||
[b"pool", bytes(base_mint), bytes(quote_mint)], LetsBonkAddresses.PROGRAM
|
||||
)
|
||||
return pool_state
|
||||
|
||||
def derive_base_vault(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey:
|
||||
|
||||
def derive_base_vault(
|
||||
self, base_mint: Pubkey, quote_mint: Pubkey | None = None
|
||||
) -> Pubkey:
|
||||
"""Derive the base vault address for a token pair.
|
||||
|
||||
|
||||
Args:
|
||||
base_mint: Base token mint address
|
||||
quote_mint: Quote token mint (defaults to WSOL)
|
||||
|
||||
|
||||
Returns:
|
||||
Base vault address
|
||||
"""
|
||||
if quote_mint is None:
|
||||
quote_mint = SystemAddresses.SOL_MINT
|
||||
|
||||
|
||||
# First derive the pool state address
|
||||
pool_state = self.derive_pool_address(base_mint, quote_mint)
|
||||
|
||||
|
||||
# Then derive the base vault using pool_vault seed
|
||||
base_vault, _ = Pubkey.find_program_address(
|
||||
[b"pool_vault", bytes(pool_state), bytes(base_mint)],
|
||||
LetsBonkAddresses.PROGRAM
|
||||
LetsBonkAddresses.PROGRAM,
|
||||
)
|
||||
return base_vault
|
||||
|
||||
def derive_quote_vault(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey:
|
||||
|
||||
def derive_quote_vault(
|
||||
self, base_mint: Pubkey, quote_mint: Pubkey | None = None
|
||||
) -> Pubkey:
|
||||
"""Derive the quote vault address for a token pair.
|
||||
|
||||
|
||||
Args:
|
||||
base_mint: Base token mint address
|
||||
quote_mint: Quote token mint (defaults to WSOL)
|
||||
|
||||
|
||||
Returns:
|
||||
Quote vault address
|
||||
"""
|
||||
if quote_mint is None:
|
||||
quote_mint = SystemAddresses.SOL_MINT
|
||||
|
||||
|
||||
# First derive the pool state address
|
||||
pool_state = self.derive_pool_address(base_mint, quote_mint)
|
||||
|
||||
|
||||
# Then derive the quote vault using pool_vault seed
|
||||
quote_vault, _ = Pubkey.find_program_address(
|
||||
[b"pool_vault", bytes(pool_state), bytes(quote_mint)],
|
||||
LetsBonkAddresses.PROGRAM
|
||||
LetsBonkAddresses.PROGRAM,
|
||||
)
|
||||
return quote_vault
|
||||
|
||||
|
||||
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
|
||||
"""Derive user's associated token account address.
|
||||
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
mint: Token mint address
|
||||
|
||||
|
||||
Returns:
|
||||
User's associated token account address
|
||||
"""
|
||||
return get_associated_token_address(user, mint)
|
||||
|
||||
|
||||
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
||||
"""Get LetsBonk-specific additional accounts needed for trading.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary of additional account addresses
|
||||
"""
|
||||
accounts = {}
|
||||
|
||||
|
||||
# Add pool state - derive if not present or use existing
|
||||
if token_info.pool_state:
|
||||
accounts["pool_state"] = token_info.pool_state
|
||||
else:
|
||||
accounts["pool_state"] = self.derive_pool_address(token_info.mint)
|
||||
|
||||
|
||||
# Add vault addresses - derive if not present or use existing
|
||||
if token_info.base_vault:
|
||||
accounts["base_vault"] = token_info.base_vault
|
||||
else:
|
||||
accounts["base_vault"] = self.derive_base_vault(token_info.mint)
|
||||
|
||||
|
||||
if token_info.quote_vault:
|
||||
accounts["quote_vault"] = token_info.quote_vault
|
||||
else:
|
||||
accounts["quote_vault"] = self.derive_quote_vault(token_info.mint)
|
||||
|
||||
|
||||
# Derive authority PDA
|
||||
accounts["authority"] = self.derive_authority_pda()
|
||||
|
||||
|
||||
# Derive event authority PDA
|
||||
accounts["event_authority"] = self.derive_event_authority_pda()
|
||||
|
||||
|
||||
return accounts
|
||||
|
||||
|
||||
def derive_authority_pda(self) -> Pubkey:
|
||||
"""Derive the authority PDA for Raydium LaunchLab.
|
||||
|
||||
|
||||
This PDA acts as the authority for pool vault operations.
|
||||
|
||||
|
||||
Returns:
|
||||
Authority PDA address
|
||||
"""
|
||||
AUTH_SEED = b"vault_auth_seed"
|
||||
authority_pda, _ = Pubkey.find_program_address(
|
||||
[AUTH_SEED],
|
||||
LetsBonkAddresses.PROGRAM
|
||||
[AUTH_SEED], LetsBonkAddresses.PROGRAM
|
||||
)
|
||||
return authority_pda
|
||||
|
||||
|
||||
def derive_event_authority_pda(self) -> Pubkey:
|
||||
"""Derive the event authority PDA for Raydium LaunchLab.
|
||||
|
||||
|
||||
This PDA is used for emitting program events during swaps.
|
||||
|
||||
|
||||
Returns:
|
||||
Event authority PDA address
|
||||
"""
|
||||
EVENT_AUTHORITY_SEED = b"__event_authority"
|
||||
event_authority_pda, _ = Pubkey.find_program_address(
|
||||
[EVENT_AUTHORITY_SEED],
|
||||
LetsBonkAddresses.PROGRAM
|
||||
[EVENT_AUTHORITY_SEED], LetsBonkAddresses.PROGRAM
|
||||
)
|
||||
return event_authority_pda
|
||||
|
||||
|
||||
def create_wsol_account_with_seed(self, payer: Pubkey, seed: str) -> Pubkey:
|
||||
"""Create a WSOL account address using createAccountWithSeed pattern.
|
||||
|
||||
|
||||
Args:
|
||||
payer: The account that will pay for and own the new account
|
||||
seed: String seed for deterministic account generation
|
||||
|
||||
|
||||
Returns:
|
||||
New WSOL account address
|
||||
"""
|
||||
return Pubkey.create_with_seed(payer, seed, SystemAddresses.TOKEN_PROGRAM)
|
||||
|
||||
def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
|
||||
def get_buy_instruction_accounts(
|
||||
self, token_info: TokenInfo, user: Pubkey
|
||||
) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a buy instruction.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for buy instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
|
||||
return {
|
||||
"payer": user,
|
||||
"authority": additional_accounts["authority"],
|
||||
@@ -243,19 +254,21 @@ class LetsBonkAddressProvider(AddressProvider):
|
||||
"event_authority": additional_accounts["event_authority"],
|
||||
"program": LetsBonkAddresses.PROGRAM,
|
||||
}
|
||||
|
||||
def get_sell_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
|
||||
def get_sell_instruction_accounts(
|
||||
self, token_info: TokenInfo, user: Pubkey
|
||||
) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a sell instruction.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for sell instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
|
||||
return {
|
||||
"payer": user,
|
||||
"authority": additional_accounts["authority"],
|
||||
@@ -272,14 +285,16 @@ class LetsBonkAddressProvider(AddressProvider):
|
||||
"event_authority": additional_accounts["event_authority"],
|
||||
"program": LetsBonkAddresses.PROGRAM,
|
||||
}
|
||||
|
||||
def get_wsol_account_creation_accounts(self, user: Pubkey, wsol_account: Pubkey) -> dict[str, Pubkey]:
|
||||
|
||||
def get_wsol_account_creation_accounts(
|
||||
self, user: Pubkey, wsol_account: Pubkey
|
||||
) -> dict[str, Pubkey]:
|
||||
"""Get accounts needed for WSOL account creation and initialization.
|
||||
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
wsol_account: WSOL account to be created
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for WSOL operations
|
||||
"""
|
||||
@@ -291,4 +306,4 @@ class LetsBonkAddressProvider(AddressProvider):
|
||||
"system_program": SystemAddresses.SYSTEM_PROGRAM,
|
||||
"token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"rent": SystemAddresses.RENT,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ logger = get_logger(__name__)
|
||||
|
||||
class LetsBonkCurveManager(CurveManager):
|
||||
"""LetsBonk (Raydium LaunchLab) implementation of CurveManager interface."""
|
||||
|
||||
|
||||
def __init__(self, client: SolanaClient, idl_parser: IDLParser):
|
||||
"""Initialize LetsBonk curve manager with injected IDL parser.
|
||||
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
idl_parser: Pre-loaded IDL parser for LetsBonk platform
|
||||
@@ -32,20 +32,20 @@ class LetsBonkCurveManager(CurveManager):
|
||||
self.client = client
|
||||
self.address_provider = LetsBonkAddressProvider()
|
||||
self._idl_parser = idl_parser
|
||||
|
||||
|
||||
logger.info("LetsBonk curve manager initialized with injected IDL parser")
|
||||
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this manager serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
|
||||
async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get the current state of a LetsBonk pool.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state account
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary containing pool state data
|
||||
"""
|
||||
@@ -53,136 +53,130 @@ class LetsBonkCurveManager(CurveManager):
|
||||
account = await self.client.get_account_info(pool_address)
|
||||
if not account.data:
|
||||
raise ValueError(f"No data in pool state account {pool_address}")
|
||||
|
||||
|
||||
# Decode pool state using injected IDL parser
|
||||
pool_state_data = self._decode_pool_state_with_idl(account.data)
|
||||
|
||||
|
||||
return pool_state_data
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Failed to get pool state")
|
||||
raise ValueError(f"Invalid pool state: {e!s}")
|
||||
|
||||
|
||||
async def calculate_price(self, pool_address: Pubkey) -> float:
|
||||
"""Calculate current token price from pool state.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
|
||||
|
||||
Returns:
|
||||
Current token price in SOL
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
|
||||
# Use virtual reserves for price calculation
|
||||
virtual_base = pool_state["virtual_base"]
|
||||
virtual_quote = pool_state["virtual_quote"]
|
||||
|
||||
|
||||
if virtual_base <= 0 or virtual_quote <= 0:
|
||||
raise ValueError("Invalid reserve state")
|
||||
|
||||
|
||||
# Price = quote_reserves / base_reserves (how much SOL per token)
|
||||
price_lamports = virtual_quote / virtual_base
|
||||
price_sol = price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
|
||||
|
||||
|
||||
return price_sol
|
||||
|
||||
|
||||
async def calculate_buy_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
self, pool_address: Pubkey, amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected tokens received for a buy operation.
|
||||
|
||||
|
||||
Uses the constant product AMM formula.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
amount_in: Amount of SOL to spend (in lamports)
|
||||
|
||||
|
||||
Returns:
|
||||
Expected amount of tokens to receive (in raw token units)
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
|
||||
virtual_base = pool_state["virtual_base"]
|
||||
virtual_quote = pool_state["virtual_quote"]
|
||||
|
||||
|
||||
# Constant product formula: tokens_out = (amount_in * virtual_base) / (virtual_quote + amount_in)
|
||||
numerator = amount_in * virtual_base
|
||||
denominator = virtual_quote + amount_in
|
||||
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
|
||||
tokens_out = numerator // denominator
|
||||
return tokens_out
|
||||
|
||||
|
||||
async def calculate_sell_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
self, pool_address: Pubkey, amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected SOL received for a sell operation.
|
||||
|
||||
|
||||
Uses the constant product AMM formula.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
amount_in: Amount of tokens to sell (in raw token units)
|
||||
|
||||
|
||||
Returns:
|
||||
Expected amount of SOL to receive (in lamports)
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
|
||||
virtual_base = pool_state["virtual_base"]
|
||||
virtual_quote = pool_state["virtual_quote"]
|
||||
|
||||
|
||||
# Constant product formula: sol_out = (amount_in * virtual_quote) / (virtual_base + amount_in)
|
||||
numerator = amount_in * virtual_quote
|
||||
denominator = virtual_base + amount_in
|
||||
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
|
||||
sol_out = numerator // denominator
|
||||
return sol_out
|
||||
|
||||
|
||||
async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]:
|
||||
"""Get current pool reserves.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (base_reserves, quote_reserves) in raw units
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
return (pool_state["virtual_base"], pool_state["virtual_quote"])
|
||||
|
||||
|
||||
def _decode_pool_state_with_idl(self, data: bytes) -> dict[str, Any]:
|
||||
"""Decode pool state data using injected IDL parser.
|
||||
|
||||
|
||||
Args:
|
||||
data: Raw account data
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary with decoded pool state
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If IDL parsing fails
|
||||
"""
|
||||
# Use injected IDL parser to decode PoolState account data
|
||||
decoded_pool_state = self._idl_parser.decode_account_data(
|
||||
data,
|
||||
"PoolState",
|
||||
skip_discriminator=True
|
||||
data, "PoolState", skip_discriminator=True
|
||||
)
|
||||
|
||||
|
||||
if not decoded_pool_state:
|
||||
raise ValueError("Failed to decode pool state with IDL parser")
|
||||
|
||||
|
||||
# Extract the fields we need for trading calculations
|
||||
# Based on the PoolState structure from the IDL
|
||||
pool_data = {
|
||||
@@ -193,28 +187,31 @@ class LetsBonkCurveManager(CurveManager):
|
||||
"status": decoded_pool_state.get("status", 0),
|
||||
"supply": decoded_pool_state.get("supply", 0),
|
||||
}
|
||||
|
||||
|
||||
# Calculate additional metrics
|
||||
if pool_data["virtual_base"] > 0:
|
||||
pool_data["price_per_token"] = (
|
||||
(pool_data["virtual_quote"] / pool_data["virtual_base"])
|
||||
* (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
|
||||
(pool_data["virtual_quote"] / pool_data["virtual_base"])
|
||||
* (10**TOKEN_DECIMALS)
|
||||
/ LAMPORTS_PER_SOL
|
||||
)
|
||||
else:
|
||||
pool_data["price_per_token"] = 0
|
||||
|
||||
logger.debug(f"Decoded pool state: virtual_base={pool_data['virtual_base']}, "
|
||||
f"virtual_quote={pool_data['virtual_quote']}, "
|
||||
f"price={pool_data['price_per_token']:.8f} SOL")
|
||||
|
||||
|
||||
logger.debug(
|
||||
f"Decoded pool state: virtual_base={pool_data['virtual_base']}, "
|
||||
f"virtual_quote={pool_data['virtual_quote']}, "
|
||||
f"price={pool_data['price_per_token']:.8f} SOL"
|
||||
)
|
||||
|
||||
return pool_data
|
||||
|
||||
|
||||
async def validate_pool_state_structure(self, pool_address: Pubkey) -> bool:
|
||||
"""Validate that the pool state structure matches IDL expectations.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the pool state
|
||||
|
||||
|
||||
Returns:
|
||||
True if structure is valid, False otherwise
|
||||
"""
|
||||
@@ -222,23 +219,27 @@ class LetsBonkCurveManager(CurveManager):
|
||||
# This would be used during development/testing to ensure
|
||||
# the IDL parsing is working correctly
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
|
||||
required_fields = [
|
||||
"virtual_base", "virtual_quote",
|
||||
"real_base", "real_quote"
|
||||
"virtual_base",
|
||||
"virtual_quote",
|
||||
"real_base",
|
||||
"real_quote",
|
||||
]
|
||||
|
||||
|
||||
for field in required_fields:
|
||||
if field not in pool_state:
|
||||
logger.error(f"Missing required field: {field}")
|
||||
return False
|
||||
|
||||
|
||||
if not isinstance(pool_state[field], int):
|
||||
logger.error(f"Field {field} is not an integer: {type(pool_state[field])}")
|
||||
logger.error(
|
||||
f"Field {field} is not an integer: {type(pool_state[field])}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
except Exception:
|
||||
logger.exception("Pool state validation failed")
|
||||
return False
|
||||
return False
|
||||
|
||||
@@ -23,59 +23,56 @@ logger = get_logger(__name__)
|
||||
|
||||
class LetsBonkEventParser(EventParser):
|
||||
"""LetsBonk implementation of EventParser interface with IDL-based parsing."""
|
||||
|
||||
|
||||
def __init__(self, idl_parser: IDLParser):
|
||||
"""Initialize LetsBonk event parser with injected IDL parser.
|
||||
|
||||
|
||||
Args:
|
||||
idl_parser: Pre-loaded IDL parser for LetsBonk platform
|
||||
"""
|
||||
self.address_provider = LetsBonkAddressProvider()
|
||||
self._idl_parser = idl_parser
|
||||
|
||||
|
||||
# Get discriminators from injected IDL parser
|
||||
discriminators = self._idl_parser.get_instruction_discriminators()
|
||||
self._initialize_discriminator_bytes = discriminators["initialize"]
|
||||
self._initialize_discriminator = struct.unpack("<Q", self._initialize_discriminator_bytes)[0]
|
||||
|
||||
self._initialize_discriminator = struct.unpack(
|
||||
"<Q", self._initialize_discriminator_bytes
|
||||
)[0]
|
||||
|
||||
logger.info("LetsBonk event parser initialized with injected IDL parser")
|
||||
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this parser serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
|
||||
def parse_token_creation_from_logs(
|
||||
self,
|
||||
logs: list[str],
|
||||
signature: str
|
||||
self, logs: list[str], signature: str
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from LetsBonk transaction logs.
|
||||
|
||||
|
||||
Args:
|
||||
logs: List of log strings from transaction
|
||||
signature: Transaction signature
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
# LetsBonk doesn't emit specific logs for token creation like pump.fun
|
||||
# Token creation is identified through instruction parsing
|
||||
return None
|
||||
|
||||
|
||||
def parse_token_creation_from_instruction(
|
||||
self,
|
||||
instruction_data: bytes,
|
||||
accounts: list[int],
|
||||
account_keys: list[bytes]
|
||||
self, instruction_data: bytes, accounts: list[int], account_keys: list[bytes]
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from LetsBonk instruction data using injected IDL parser.
|
||||
|
||||
|
||||
Args:
|
||||
instruction_data: Raw instruction data
|
||||
accounts: List of account indices
|
||||
account_keys: List of account public keys
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
@@ -93,14 +90,16 @@ class LetsBonkEventParser(EventParser):
|
||||
return Pubkey.from_bytes(account_keys[account_index])
|
||||
|
||||
# Parse instruction data using injected IDL parser
|
||||
decoded = self._idl_parser.decode_instruction(instruction_data, account_keys, accounts)
|
||||
if not decoded or decoded['instruction_name'] != 'initialize':
|
||||
decoded = self._idl_parser.decode_instruction(
|
||||
instruction_data, account_keys, accounts
|
||||
)
|
||||
if not decoded or decoded["instruction_name"] != "initialize":
|
||||
return None
|
||||
|
||||
args = decoded.get('args', {})
|
||||
|
||||
|
||||
args = decoded.get("args", {})
|
||||
|
||||
# Extract MintParams from the decoded arguments
|
||||
base_mint_param = args.get('base_mint_param', {})
|
||||
base_mint_param = args.get("base_mint_param", {})
|
||||
if not base_mint_param:
|
||||
return None
|
||||
|
||||
@@ -109,7 +108,7 @@ class LetsBonkEventParser(EventParser):
|
||||
# 0: creator (signer)
|
||||
# 1: creator_ata (not needed for TokenInfo)
|
||||
# 2: global_config
|
||||
# 3: platform_config
|
||||
# 3: platform_config
|
||||
# 4: creator
|
||||
# 5: pool_state
|
||||
# 6: base_mint
|
||||
@@ -117,7 +116,7 @@ class LetsBonkEventParser(EventParser):
|
||||
# 8: base_vault
|
||||
# 9: quote_vault
|
||||
# ... other accounts
|
||||
|
||||
|
||||
creator = get_account_key(0) # First signer account (creator)
|
||||
pool_state = get_account_key(5) # pool_state account
|
||||
base_mint = get_account_key(6) # base_mint account
|
||||
@@ -125,8 +124,10 @@ class LetsBonkEventParser(EventParser):
|
||||
quote_vault = get_account_key(9) # quote_vault account
|
||||
|
||||
if not all([creator, pool_state, base_mint, base_vault, quote_vault]):
|
||||
logger.debug(f"Missing required accounts: creator={creator}, pool_state={pool_state}, "
|
||||
f"base_mint={base_mint}, base_vault={base_vault}, quote_vault={quote_vault}")
|
||||
logger.debug(
|
||||
f"Missing required accounts: creator={creator}, pool_state={pool_state}, "
|
||||
f"base_mint={base_mint}, base_vault={base_vault}, quote_vault={quote_vault}"
|
||||
)
|
||||
return None
|
||||
|
||||
return TokenInfo(
|
||||
@@ -146,21 +147,20 @@ class LetsBonkEventParser(EventParser):
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse initialize instruction: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_token_creation_from_geyser(
|
||||
self,
|
||||
transaction_info: Any
|
||||
self, transaction_info: Any
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from Geyser transaction data.
|
||||
|
||||
|
||||
Args:
|
||||
transaction_info: Geyser transaction information
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
try:
|
||||
if not hasattr(transaction_info, 'transaction'):
|
||||
if not hasattr(transaction_info, "transaction"):
|
||||
return None
|
||||
|
||||
tx = transaction_info.transaction.transaction.transaction
|
||||
@@ -183,10 +183,14 @@ class LetsBonkEventParser(EventParser):
|
||||
for acc_idx in ix.accounts:
|
||||
if acc_idx < len(msg.account_keys):
|
||||
acc_key = msg.account_keys[acc_idx]
|
||||
if bytes(acc_key) == bytes(self.address_provider.get_system_addresses()["platform_config"]):
|
||||
if bytes(acc_key) == bytes(
|
||||
self.address_provider.get_system_addresses()[
|
||||
"platform_config"
|
||||
]
|
||||
):
|
||||
has_platform_config = True
|
||||
break
|
||||
|
||||
|
||||
if not has_platform_config:
|
||||
continue
|
||||
|
||||
@@ -202,29 +206,29 @@ class LetsBonkEventParser(EventParser):
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse geyser transaction: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_program_id(self) -> Pubkey:
|
||||
"""Get the Raydium LaunchLab program ID this parser monitors.
|
||||
|
||||
|
||||
Returns:
|
||||
Raydium LaunchLab program ID
|
||||
"""
|
||||
return self.address_provider.program_id
|
||||
|
||||
|
||||
def get_instruction_discriminators(self) -> list[bytes]:
|
||||
"""Get instruction discriminators for token creation.
|
||||
|
||||
|
||||
Returns:
|
||||
List of discriminator bytes to match
|
||||
"""
|
||||
return [self._initialize_discriminator_bytes]
|
||||
|
||||
|
||||
def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
|
||||
"""Parse token creation from block data (for block listener).
|
||||
|
||||
|
||||
Args:
|
||||
block_data: Block data from WebSocket
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
@@ -243,82 +247,95 @@ class LetsBonkEventParser(EventParser):
|
||||
tx_data_encoded = tx_data[0]
|
||||
tx_data_decoded = base64.b64decode(tx_data_encoded)
|
||||
transaction = VersionedTransaction.from_bytes(tx_data_decoded)
|
||||
|
||||
|
||||
for ix in transaction.message.instructions:
|
||||
program_id = transaction.message.account_keys[ix.program_id_index]
|
||||
|
||||
program_id = transaction.message.account_keys[
|
||||
ix.program_id_index
|
||||
]
|
||||
|
||||
# Check if instruction is from LetsBonk program
|
||||
if str(program_id) != str(self.get_program_id()):
|
||||
continue
|
||||
|
||||
ix_data = bytes(ix.data)
|
||||
|
||||
|
||||
# Check for initialize discriminator
|
||||
if len(ix_data) >= 8:
|
||||
discriminator = struct.unpack("<Q", ix_data[:8])[0]
|
||||
|
||||
|
||||
if discriminator == self._initialize_discriminator:
|
||||
# Token creation should have substantial data and many accounts
|
||||
if len(ix_data) <= 8 or len(ix.accounts) < 10:
|
||||
continue
|
||||
|
||||
|
||||
account_keys_bytes = [
|
||||
bytes(key)
|
||||
for key in transaction.message.account_keys
|
||||
]
|
||||
|
||||
|
||||
# Parse the instruction
|
||||
token_info = self.parse_token_creation_from_instruction(
|
||||
ix_data, ix.accounts, account_keys_bytes
|
||||
token_info = (
|
||||
self.parse_token_creation_from_instruction(
|
||||
ix_data, ix.accounts, account_keys_bytes
|
||||
)
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse block transaction: {e}")
|
||||
continue
|
||||
|
||||
|
||||
# Handle already decoded transaction data
|
||||
elif isinstance(tx_data, dict) and "message" in tx_data:
|
||||
try:
|
||||
message = tx_data["message"]
|
||||
if "instructions" not in message or "accountKeys" not in message:
|
||||
if (
|
||||
"instructions" not in message
|
||||
or "accountKeys" not in message
|
||||
):
|
||||
continue
|
||||
|
||||
|
||||
for ix in message["instructions"]:
|
||||
if "programIdIndex" not in ix or "accounts" not in ix or "data" not in ix:
|
||||
if (
|
||||
"programIdIndex" not in ix
|
||||
or "accounts" not in ix
|
||||
or "data" not in ix
|
||||
):
|
||||
continue
|
||||
|
||||
|
||||
program_idx = ix["programIdIndex"]
|
||||
if program_idx >= len(message["accountKeys"]):
|
||||
continue
|
||||
|
||||
|
||||
program_id_str = message["accountKeys"][program_idx]
|
||||
if program_id_str != str(self.get_program_id()):
|
||||
continue
|
||||
|
||||
|
||||
# Decode instruction data
|
||||
ix_data = base64.b64decode(ix["data"])
|
||||
|
||||
|
||||
if len(ix_data) >= 8:
|
||||
discriminator = struct.unpack("<Q", ix_data[:8])[0]
|
||||
|
||||
|
||||
if discriminator == self._initialize_discriminator:
|
||||
if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
|
||||
continue
|
||||
|
||||
|
||||
# Convert account keys to bytes for parsing
|
||||
account_keys_bytes = [
|
||||
Pubkey.from_string(key).to_bytes()
|
||||
Pubkey.from_string(key).to_bytes()
|
||||
for key in message["accountKeys"]
|
||||
]
|
||||
|
||||
token_info = self.parse_token_creation_from_instruction(
|
||||
ix_data, ix["accounts"], account_keys_bytes
|
||||
|
||||
token_info = (
|
||||
self.parse_token_creation_from_instruction(
|
||||
ix_data, ix["accounts"], account_keys_bytes
|
||||
)
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse decoded block transaction: {e}")
|
||||
continue
|
||||
@@ -327,4 +344,4 @@ class LetsBonkEventParser(EventParser):
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse block data: {e}")
|
||||
return None
|
||||
return None
|
||||
|
||||
@@ -29,69 +29,69 @@ logger = get_logger(__name__)
|
||||
|
||||
class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
"""LetsBonk (Raydium LaunchLab) implementation of InstructionBuilder interface with IDL-based discriminators."""
|
||||
|
||||
|
||||
def __init__(self, idl_parser: IDLParser):
|
||||
"""Initialize LetsBonk instruction builder with injected IDL parser.
|
||||
|
||||
|
||||
Args:
|
||||
idl_parser: Pre-loaded IDL parser for LetsBonk platform
|
||||
"""
|
||||
self._idl_parser = idl_parser
|
||||
|
||||
|
||||
# Get discriminators from injected IDL parser
|
||||
discriminators = self._idl_parser.get_instruction_discriminators()
|
||||
self._buy_exact_in_discriminator = discriminators["buy_exact_in"]
|
||||
self._sell_exact_in_discriminator = discriminators["sell_exact_in"]
|
||||
|
||||
|
||||
logger.info("LetsBonk instruction builder initialized with injected IDL parser")
|
||||
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this builder serves."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
|
||||
async def build_buy_instruction(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
address_provider: AddressProvider,
|
||||
) -> list[Instruction]:
|
||||
"""Build buy instruction(s) for LetsBonk using buy_exact_in.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of SOL to spend (in lamports)
|
||||
minimum_amount_out: Minimum tokens expected (raw token units)
|
||||
address_provider: Platform address provider
|
||||
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the buy operation
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
|
||||
# Get all required accounts
|
||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||
|
||||
|
||||
# 1. Create idempotent ATA for base token
|
||||
ata_instruction = create_idempotent_associated_token_account(
|
||||
user, # payer
|
||||
user, # owner
|
||||
user, # owner
|
||||
token_info.mint, # mint
|
||||
SystemAddresses.TOKEN_PROGRAM, # token program
|
||||
)
|
||||
instructions.append(ata_instruction)
|
||||
|
||||
|
||||
# 2. Create WSOL account with seed (temporary account for the transaction)
|
||||
wsol_seed = self._generate_wsol_seed(user)
|
||||
wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed)
|
||||
|
||||
|
||||
# Account creation cost + amount to spend
|
||||
account_creation_lamports = TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE
|
||||
total_lamports = amount_in + account_creation_lamports
|
||||
|
||||
|
||||
create_wsol_ix = create_account_with_seed(
|
||||
CreateAccountWithSeedParams(
|
||||
from_pubkey=user,
|
||||
@@ -100,97 +100,129 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
seed=wsol_seed,
|
||||
lamports=total_lamports,
|
||||
space=TOKEN_ACCOUNT_SIZE,
|
||||
owner=SystemAddresses.TOKEN_PROGRAM
|
||||
owner=SystemAddresses.TOKEN_PROGRAM,
|
||||
)
|
||||
)
|
||||
instructions.append(create_wsol_ix)
|
||||
|
||||
|
||||
# 3. Initialize WSOL account
|
||||
initialize_wsol_ix = self._create_initialize_account_instruction(
|
||||
wsol_account,
|
||||
SystemAddresses.SOL_MINT,
|
||||
user
|
||||
wsol_account, SystemAddresses.SOL_MINT, user
|
||||
)
|
||||
instructions.append(initialize_wsol_ix)
|
||||
|
||||
|
||||
# 4. Build buy_exact_in instruction with correct account ordering
|
||||
# Based on the IDL and manual examples, the account order should be:
|
||||
buy_accounts = [
|
||||
AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer
|
||||
AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), # authority
|
||||
AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), # global_config
|
||||
AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), # platform_config
|
||||
AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), # pool_state
|
||||
AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), # user_base_token
|
||||
AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token (WSOL account)
|
||||
AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), # base_vault
|
||||
AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), # quote_vault
|
||||
AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), # base_token_mint
|
||||
AccountMeta(pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False), # quote_token_mint
|
||||
AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # base_token_program
|
||||
AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # quote_token_program
|
||||
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), # event_authority
|
||||
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), # program
|
||||
AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["authority"], is_signer=False, is_writable=False
|
||||
), # authority
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["global_config"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
), # global_config
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["platform_config"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
), # platform_config
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True
|
||||
), # pool_state
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["user_base_token"],
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
), # user_base_token
|
||||
AccountMeta(
|
||||
pubkey=wsol_account, is_signer=False, is_writable=True
|
||||
), # user_quote_token (WSOL account)
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True
|
||||
), # base_vault
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True
|
||||
), # quote_vault
|
||||
AccountMeta(
|
||||
pubkey=token_info.mint, is_signer=False, is_writable=False
|
||||
), # base_token_mint
|
||||
AccountMeta(
|
||||
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
|
||||
), # quote_token_mint
|
||||
AccountMeta(
|
||||
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
|
||||
), # base_token_program
|
||||
AccountMeta(
|
||||
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
|
||||
), # quote_token_program
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["event_authority"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
), # event_authority
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["program"], is_signer=False, is_writable=False
|
||||
), # program
|
||||
]
|
||||
|
||||
|
||||
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
|
||||
SHARE_FEE_RATE = 0 # No sharing fee
|
||||
instruction_data = (
|
||||
self._buy_exact_in_discriminator +
|
||||
struct.pack("<Q", amount_in) + # amount_in (u64) - SOL to spend
|
||||
struct.pack("<Q", minimum_amount_out) + # minimum_amount_out (u64) - min tokens
|
||||
struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
|
||||
self._buy_exact_in_discriminator
|
||||
+ struct.pack("<Q", amount_in) # amount_in (u64) - SOL to spend
|
||||
+ struct.pack(
|
||||
"<Q", minimum_amount_out
|
||||
) # minimum_amount_out (u64) - min tokens
|
||||
+ struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
|
||||
)
|
||||
|
||||
|
||||
buy_instruction = Instruction(
|
||||
program_id=accounts_info["program"],
|
||||
data=instruction_data,
|
||||
accounts=buy_accounts
|
||||
accounts=buy_accounts,
|
||||
)
|
||||
instructions.append(buy_instruction)
|
||||
|
||||
|
||||
# 5. Close WSOL account to reclaim SOL
|
||||
close_wsol_ix = self._create_close_account_instruction(
|
||||
wsol_account,
|
||||
user,
|
||||
user
|
||||
)
|
||||
close_wsol_ix = self._create_close_account_instruction(wsol_account, user, user)
|
||||
instructions.append(close_wsol_ix)
|
||||
|
||||
|
||||
return instructions
|
||||
|
||||
|
||||
async def build_sell_instruction(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
address_provider: AddressProvider,
|
||||
) -> list[Instruction]:
|
||||
"""Build sell instruction(s) for LetsBonk using sell_exact_in.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of tokens to sell (raw token units)
|
||||
minimum_amount_out: Minimum SOL expected (in lamports)
|
||||
address_provider: Platform address provider
|
||||
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the sell operation
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
|
||||
# Get all required accounts
|
||||
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
||||
|
||||
|
||||
# 1. Create WSOL account with seed (to receive SOL)
|
||||
wsol_seed = self._generate_wsol_seed(user)
|
||||
wsol_account = address_provider.create_wsol_account_with_seed(user, wsol_seed)
|
||||
|
||||
|
||||
# Minimal account creation cost
|
||||
account_creation_lamports = TOKEN_ACCOUNT_RENT_EXEMPT_RESERVE
|
||||
|
||||
|
||||
create_wsol_ix = create_account_with_seed(
|
||||
CreateAccountWithSeedParams(
|
||||
from_pubkey=user,
|
||||
@@ -199,82 +231,111 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
seed=wsol_seed,
|
||||
lamports=account_creation_lamports,
|
||||
space=TOKEN_ACCOUNT_SIZE,
|
||||
owner=SystemAddresses.TOKEN_PROGRAM
|
||||
owner=SystemAddresses.TOKEN_PROGRAM,
|
||||
)
|
||||
)
|
||||
instructions.append(create_wsol_ix)
|
||||
|
||||
|
||||
# 2. Initialize WSOL account
|
||||
initialize_wsol_ix = self._create_initialize_account_instruction(
|
||||
wsol_account,
|
||||
SystemAddresses.SOL_MINT,
|
||||
user
|
||||
wsol_account, SystemAddresses.SOL_MINT, user
|
||||
)
|
||||
instructions.append(initialize_wsol_ix)
|
||||
|
||||
|
||||
# 3. Build sell_exact_in instruction with correct account ordering
|
||||
sell_accounts = [
|
||||
AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer
|
||||
AccountMeta(pubkey=accounts_info["authority"], is_signer=False, is_writable=False), # authority
|
||||
AccountMeta(pubkey=accounts_info["global_config"], is_signer=False, is_writable=False), # global_config
|
||||
AccountMeta(pubkey=accounts_info["platform_config"], is_signer=False, is_writable=False), # platform_config
|
||||
AccountMeta(pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True), # pool_state
|
||||
AccountMeta(pubkey=accounts_info["user_base_token"], is_signer=False, is_writable=True), # user_base_token (tokens being sold)
|
||||
AccountMeta(pubkey=wsol_account, is_signer=False, is_writable=True), # user_quote_token (WSOL received)
|
||||
AccountMeta(pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True), # base_vault (receives tokens)
|
||||
AccountMeta(pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True), # quote_vault (sends WSOL)
|
||||
AccountMeta(pubkey=token_info.mint, is_signer=False, is_writable=False), # base_token_mint
|
||||
AccountMeta(pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False), # quote_token_mint
|
||||
AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # base_token_program
|
||||
AccountMeta(pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False), # quote_token_program
|
||||
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False), # event_authority
|
||||
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False), # program
|
||||
AccountMeta(pubkey=user, is_signer=True, is_writable=True), # payer
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["authority"], is_signer=False, is_writable=False
|
||||
), # authority
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["global_config"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
), # global_config
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["platform_config"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
), # platform_config
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["pool_state"], is_signer=False, is_writable=True
|
||||
), # pool_state
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["user_base_token"],
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
), # user_base_token (tokens being sold)
|
||||
AccountMeta(
|
||||
pubkey=wsol_account, is_signer=False, is_writable=True
|
||||
), # user_quote_token (WSOL received)
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["base_vault"], is_signer=False, is_writable=True
|
||||
), # base_vault (receives tokens)
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["quote_vault"], is_signer=False, is_writable=True
|
||||
), # quote_vault (sends WSOL)
|
||||
AccountMeta(
|
||||
pubkey=token_info.mint, is_signer=False, is_writable=False
|
||||
), # base_token_mint
|
||||
AccountMeta(
|
||||
pubkey=SystemAddresses.SOL_MINT, is_signer=False, is_writable=False
|
||||
), # quote_token_mint
|
||||
AccountMeta(
|
||||
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
|
||||
), # base_token_program
|
||||
AccountMeta(
|
||||
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
|
||||
), # quote_token_program
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["event_authority"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
), # event_authority
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["program"], is_signer=False, is_writable=False
|
||||
), # program
|
||||
]
|
||||
|
||||
|
||||
# Build instruction data: discriminator + amount_in + minimum_amount_out + share_fee_rate
|
||||
SHARE_FEE_RATE = 0 # No sharing fee
|
||||
instruction_data = (
|
||||
self._sell_exact_in_discriminator +
|
||||
struct.pack("<Q", amount_in) + # amount_in (u64) - tokens to sell
|
||||
struct.pack("<Q", minimum_amount_out) + # minimum_amount_out (u64) - min SOL
|
||||
struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
|
||||
self._sell_exact_in_discriminator
|
||||
+ struct.pack("<Q", amount_in) # amount_in (u64) - tokens to sell
|
||||
+ struct.pack(
|
||||
"<Q", minimum_amount_out
|
||||
) # minimum_amount_out (u64) - min SOL
|
||||
+ struct.pack("<Q", SHARE_FEE_RATE) # share_fee_rate (u64): 0
|
||||
)
|
||||
|
||||
|
||||
sell_instruction = Instruction(
|
||||
program_id=accounts_info["program"],
|
||||
data=instruction_data,
|
||||
accounts=sell_accounts
|
||||
accounts=sell_accounts,
|
||||
)
|
||||
instructions.append(sell_instruction)
|
||||
|
||||
|
||||
# 4. Close WSOL account to reclaim SOL
|
||||
close_wsol_ix = self._create_close_account_instruction(
|
||||
wsol_account,
|
||||
user,
|
||||
user
|
||||
)
|
||||
close_wsol_ix = self._create_close_account_instruction(wsol_account, user, user)
|
||||
instructions.append(close_wsol_ix)
|
||||
|
||||
|
||||
return instructions
|
||||
|
||||
|
||||
def get_required_accounts_for_buy(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for buy operation (for priority fee calculation).
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||
|
||||
|
||||
return [
|
||||
accounts_info["pool_state"],
|
||||
accounts_info["user_base_token"],
|
||||
@@ -284,25 +345,22 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
SystemAddresses.SOL_MINT,
|
||||
accounts_info["program"],
|
||||
]
|
||||
|
||||
|
||||
def get_required_accounts_for_sell(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for sell operation (for priority fee calculation).
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
||||
|
||||
|
||||
return [
|
||||
accounts_info["pool_state"],
|
||||
accounts_info["user_base_token"],
|
||||
@@ -312,33 +370,30 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
SystemAddresses.SOL_MINT,
|
||||
accounts_info["program"],
|
||||
]
|
||||
|
||||
|
||||
def _generate_wsol_seed(self, user: Pubkey) -> str:
|
||||
"""Generate a unique seed for WSOL account creation.
|
||||
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
|
||||
|
||||
Returns:
|
||||
Unique seed string for WSOL account
|
||||
"""
|
||||
# Generate a unique seed based on timestamp and user pubkey
|
||||
seed_data = f"{int(time.time())}{user!s}"
|
||||
return hashlib.sha256(seed_data.encode()).hexdigest()[:32]
|
||||
|
||||
|
||||
def _create_initialize_account_instruction(
|
||||
self,
|
||||
account: Pubkey,
|
||||
mint: Pubkey,
|
||||
owner: Pubkey
|
||||
self, account: Pubkey, mint: Pubkey, owner: Pubkey
|
||||
) -> Instruction:
|
||||
"""Create an InitializeAccount instruction for the Token Program.
|
||||
|
||||
|
||||
Args:
|
||||
account: The account to initialize
|
||||
mint: The token mint
|
||||
owner: The account owner
|
||||
|
||||
|
||||
Returns:
|
||||
Instruction for initializing the account
|
||||
"""
|
||||
@@ -346,31 +401,28 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
AccountMeta(pubkey=account, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=owner, is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=SystemAddresses.RENT, is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
pubkey=SystemAddresses.RENT, is_signer=False, is_writable=False
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# InitializeAccount instruction discriminator (instruction 1 in Token Program)
|
||||
data = bytes([1])
|
||||
|
||||
|
||||
return Instruction(
|
||||
program_id=SystemAddresses.TOKEN_PROGRAM,
|
||||
data=data,
|
||||
accounts=accounts
|
||||
program_id=SystemAddresses.TOKEN_PROGRAM, data=data, accounts=accounts
|
||||
)
|
||||
|
||||
|
||||
def _create_close_account_instruction(
|
||||
self,
|
||||
account: Pubkey,
|
||||
destination: Pubkey,
|
||||
owner: Pubkey
|
||||
self, account: Pubkey, destination: Pubkey, owner: Pubkey
|
||||
) -> Instruction:
|
||||
"""Create a CloseAccount instruction for the Token Program.
|
||||
|
||||
|
||||
Args:
|
||||
account: The account to close
|
||||
destination: Where to send the remaining lamports
|
||||
owner: The account owner (must sign)
|
||||
|
||||
|
||||
Returns:
|
||||
Instruction for closing the account
|
||||
"""
|
||||
@@ -379,34 +431,32 @@ class LetsBonkInstructionBuilder(InstructionBuilder):
|
||||
AccountMeta(pubkey=destination, is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=owner, is_signer=True, is_writable=False),
|
||||
]
|
||||
|
||||
|
||||
# CloseAccount instruction discriminator (instruction 9 in Token Program)
|
||||
data = bytes([9])
|
||||
|
||||
|
||||
return Instruction(
|
||||
program_id=SystemAddresses.TOKEN_PROGRAM,
|
||||
data=data,
|
||||
accounts=accounts
|
||||
program_id=SystemAddresses.TOKEN_PROGRAM, data=data, accounts=accounts
|
||||
)
|
||||
|
||||
|
||||
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
|
||||
"""Convert decimal token amount to raw token units.
|
||||
|
||||
|
||||
Args:
|
||||
token_amount_decimal: Token amount in decimal form
|
||||
|
||||
|
||||
Returns:
|
||||
Token amount in raw units (adjusted for decimals)
|
||||
"""
|
||||
return int(token_amount_decimal * 10**TOKEN_DECIMALS)
|
||||
|
||||
|
||||
def calculate_token_amount_decimal(self, token_amount_raw: int) -> float:
|
||||
"""Convert raw token amount to decimal form.
|
||||
|
||||
|
||||
Args:
|
||||
token_amount_raw: Token amount in raw units
|
||||
|
||||
|
||||
Returns:
|
||||
Token amount in decimal form
|
||||
"""
|
||||
return token_amount_raw / 10**TOKEN_DECIMALS
|
||||
return token_amount_raw / 10**TOKEN_DECIMALS
|
||||
|
||||
@@ -14,39 +14,39 @@ logger = get_logger(__name__)
|
||||
|
||||
class LetsBonkPumpPortalProcessor:
|
||||
"""PumpPortal processor for LetsBonk tokens."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the processor with address provider."""
|
||||
self.address_provider = LetsBonkAddressProvider()
|
||||
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this processor handles."""
|
||||
return Platform.LETS_BONK
|
||||
|
||||
|
||||
@property
|
||||
def supported_pool_names(self) -> list[str]:
|
||||
"""Get the pool names this processor supports from PumpPortal."""
|
||||
return ["bonk"] # PumpPortal pool name for LetsBonk/bonk pools
|
||||
|
||||
|
||||
def can_process(self, token_data: dict) -> bool:
|
||||
"""Check if this processor can handle the given token data.
|
||||
|
||||
|
||||
Args:
|
||||
token_data: Token data from PumpPortal
|
||||
|
||||
|
||||
Returns:
|
||||
True if this processor can handle the token data
|
||||
"""
|
||||
pool = token_data.get("pool", "").lower()
|
||||
return pool in self.supported_pool_names
|
||||
|
||||
|
||||
def process_token_data(self, token_data: dict) -> TokenInfo | None:
|
||||
"""Process LetsBonk token data from PumpPortal.
|
||||
|
||||
|
||||
Args:
|
||||
token_data: Token data from PumpPortal WebSocket
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
@@ -62,7 +62,9 @@ class LetsBonkPumpPortalProcessor:
|
||||
# This would need to be adjusted based on actual PumpPortal data for LetsBonk tokens
|
||||
|
||||
if not all([name, symbol, mint_str, creator_str]):
|
||||
logger.warning("Missing required fields in PumpPortal LetsBonk token data")
|
||||
logger.warning(
|
||||
"Missing required fields in PumpPortal LetsBonk token data"
|
||||
)
|
||||
return None
|
||||
|
||||
# Convert string addresses to Pubkey objects
|
||||
@@ -72,7 +74,7 @@ class LetsBonkPumpPortalProcessor:
|
||||
|
||||
# Derive LetsBonk-specific addresses
|
||||
pool_state = self.address_provider.derive_pool_address(mint)
|
||||
|
||||
|
||||
# For LetsBonk, vault addresses might need to be derived differently
|
||||
# or provided in the PumpPortal data. For now, we'll derive them
|
||||
# using the standard pattern, but this might need adjustment
|
||||
@@ -91,15 +93,15 @@ class LetsBonkPumpPortalProcessor:
|
||||
quote_vault=None, # Will be filled from additional_accounts
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Extract vault addresses if available
|
||||
base_vault = additional_accounts.get("base_vault")
|
||||
quote_vault = additional_accounts.get("quote_vault")
|
||||
|
||||
# If vaults aren't available from additional_accounts,
|
||||
|
||||
# If vaults aren't available from additional_accounts,
|
||||
# we might need to derive them or leave them None
|
||||
# and let the trading logic handle the derivation
|
||||
|
||||
|
||||
return TokenInfo(
|
||||
name=name,
|
||||
symbol=symbol,
|
||||
@@ -115,4 +117,4 @@ class LetsBonkPumpPortalProcessor:
|
||||
|
||||
except Exception:
|
||||
logger.exception("Failed to process PumpPortal LetsBonk token data")
|
||||
return None
|
||||
return None
|
||||
|
||||
@@ -13,9 +13,9 @@ from .pumpportal_processor import PumpFunPumpPortalProcessor
|
||||
|
||||
# Export implementations for direct use if needed
|
||||
__all__ = [
|
||||
'PumpFunAddressProvider',
|
||||
'PumpFunCurveManager',
|
||||
'PumpFunEventParser',
|
||||
'PumpFunInstructionBuilder',
|
||||
'PumpFunPumpPortalProcessor'
|
||||
]
|
||||
"PumpFunAddressProvider",
|
||||
"PumpFunCurveManager",
|
||||
"PumpFunEventParser",
|
||||
"PumpFunInstructionBuilder",
|
||||
"PumpFunPumpPortalProcessor",
|
||||
]
|
||||
|
||||
@@ -39,7 +39,7 @@ class PumpFunAddresses:
|
||||
def find_global_volume_accumulator() -> Pubkey:
|
||||
"""
|
||||
Derive the Program Derived Address (PDA) for the global volume accumulator.
|
||||
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived global volume accumulator account
|
||||
"""
|
||||
@@ -53,10 +53,10 @@ class PumpFunAddresses:
|
||||
def find_user_volume_accumulator(user: Pubkey) -> Pubkey:
|
||||
"""
|
||||
Derive the Program Derived Address (PDA) for a user's volume accumulator.
|
||||
|
||||
|
||||
Args:
|
||||
user: Pubkey of the user account
|
||||
|
||||
|
||||
Returns:
|
||||
Pubkey of the derived user volume accumulator account
|
||||
"""
|
||||
@@ -69,26 +69,26 @@ class PumpFunAddresses:
|
||||
|
||||
class PumpFunAddressProvider(AddressProvider):
|
||||
"""Pump.Fun implementation of AddressProvider interface."""
|
||||
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this provider serves."""
|
||||
return Platform.PUMP_FUN
|
||||
|
||||
|
||||
@property
|
||||
def program_id(self) -> Pubkey:
|
||||
"""Get the main program ID for this platform."""
|
||||
return PumpFunAddresses.PROGRAM
|
||||
|
||||
|
||||
def get_system_addresses(self) -> dict[str, Pubkey]:
|
||||
"""Get all system addresses required for pump.fun.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary mapping address names to Pubkey objects
|
||||
"""
|
||||
# Get system addresses from the single source of truth
|
||||
system_addresses = SystemAddresses.get_all_system_addresses()
|
||||
|
||||
|
||||
# Add pump.fun specific addresses
|
||||
pumpfun_addresses = {
|
||||
# Pump.fun specific addresses
|
||||
@@ -98,82 +98,85 @@ class PumpFunAddressProvider(AddressProvider):
|
||||
"fee": PumpFunAddresses.FEE,
|
||||
"liquidity_migrator": PumpFunAddresses.LIQUIDITY_MIGRATOR,
|
||||
}
|
||||
|
||||
|
||||
# Combine system and platform-specific addresses
|
||||
return {**system_addresses, **pumpfun_addresses}
|
||||
|
||||
def derive_pool_address(self, base_mint: Pubkey, quote_mint: Pubkey | None = None) -> Pubkey:
|
||||
|
||||
def derive_pool_address(
|
||||
self, base_mint: Pubkey, quote_mint: Pubkey | None = None
|
||||
) -> Pubkey:
|
||||
"""Derive the bonding curve address for a token.
|
||||
|
||||
|
||||
For pump.fun, this is the bonding curve PDA derived from the mint.
|
||||
|
||||
|
||||
Args:
|
||||
base_mint: Token mint address
|
||||
quote_mint: Not used for pump.fun (SOL is always the quote)
|
||||
|
||||
|
||||
Returns:
|
||||
Bonding curve address
|
||||
"""
|
||||
bonding_curve, _ = Pubkey.find_program_address(
|
||||
[b"bonding-curve", bytes(base_mint)],
|
||||
PumpFunAddresses.PROGRAM
|
||||
[b"bonding-curve", bytes(base_mint)], PumpFunAddresses.PROGRAM
|
||||
)
|
||||
return bonding_curve
|
||||
|
||||
|
||||
def derive_user_token_account(self, user: Pubkey, mint: Pubkey) -> Pubkey:
|
||||
"""Derive user's associated token account address.
|
||||
|
||||
|
||||
Args:
|
||||
user: User's wallet address
|
||||
mint: Token mint address
|
||||
|
||||
|
||||
Returns:
|
||||
User's associated token account address
|
||||
"""
|
||||
return get_associated_token_address(user, mint)
|
||||
|
||||
|
||||
def get_additional_accounts(self, token_info: TokenInfo) -> dict[str, Pubkey]:
|
||||
"""Get pump.fun-specific additional accounts needed for trading.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary of additional account addresses
|
||||
"""
|
||||
accounts = {}
|
||||
|
||||
|
||||
# Add bonding curve if available
|
||||
if token_info.bonding_curve:
|
||||
accounts["bonding_curve"] = token_info.bonding_curve
|
||||
|
||||
|
||||
# Add associated bonding curve if available
|
||||
if token_info.associated_bonding_curve:
|
||||
accounts["associated_bonding_curve"] = token_info.associated_bonding_curve
|
||||
|
||||
|
||||
# Add creator vault if available
|
||||
if token_info.creator_vault:
|
||||
accounts["creator_vault"] = token_info.creator_vault
|
||||
|
||||
|
||||
# Derive associated bonding curve if not provided
|
||||
if not token_info.associated_bonding_curve and token_info.bonding_curve:
|
||||
accounts["associated_bonding_curve"] = self.derive_associated_bonding_curve(
|
||||
token_info.mint, token_info.bonding_curve
|
||||
)
|
||||
|
||||
|
||||
# Derive creator vault if not provided but creator is available
|
||||
if not token_info.creator_vault and token_info.creator:
|
||||
accounts["creator_vault"] = self.derive_creator_vault(token_info.creator)
|
||||
|
||||
|
||||
return accounts
|
||||
|
||||
def derive_associated_bonding_curve(self, mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
|
||||
|
||||
def derive_associated_bonding_curve(
|
||||
self, mint: Pubkey, bonding_curve: Pubkey
|
||||
) -> Pubkey:
|
||||
"""Derive the associated bonding curve (ATA of bonding curve for the token).
|
||||
|
||||
|
||||
Args:
|
||||
mint: Token mint address
|
||||
bonding_curve: Bonding curve address
|
||||
|
||||
|
||||
Returns:
|
||||
Associated bonding curve address
|
||||
"""
|
||||
@@ -186,93 +189,108 @@ class PumpFunAddressProvider(AddressProvider):
|
||||
SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
|
||||
|
||||
def derive_creator_vault(self, creator: Pubkey) -> Pubkey:
|
||||
"""Derive the creator vault address.
|
||||
|
||||
|
||||
Args:
|
||||
creator: Creator address
|
||||
|
||||
|
||||
Returns:
|
||||
Creator vault address
|
||||
"""
|
||||
creator_vault, _ = Pubkey.find_program_address(
|
||||
[b"creator-vault", bytes(creator)],
|
||||
PumpFunAddresses.PROGRAM
|
||||
[b"creator-vault", bytes(creator)], PumpFunAddresses.PROGRAM
|
||||
)
|
||||
return creator_vault
|
||||
|
||||
|
||||
def derive_global_volume_accumulator(self) -> Pubkey:
|
||||
"""Derive the global volume accumulator PDA.
|
||||
|
||||
|
||||
Returns:
|
||||
Global volume accumulator address
|
||||
"""
|
||||
return PumpFunAddresses.find_global_volume_accumulator()
|
||||
|
||||
|
||||
def derive_user_volume_accumulator(self, user: Pubkey) -> Pubkey:
|
||||
"""Derive the user volume accumulator PDA.
|
||||
|
||||
|
||||
Args:
|
||||
user: User address
|
||||
|
||||
|
||||
Returns:
|
||||
User volume accumulator address
|
||||
"""
|
||||
return PumpFunAddresses.find_user_volume_accumulator(user)
|
||||
|
||||
def get_buy_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
|
||||
def get_buy_instruction_accounts(
|
||||
self, token_info: TokenInfo, user: Pubkey
|
||||
) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a buy instruction.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for buy instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
|
||||
return {
|
||||
"global": PumpFunAddresses.GLOBAL,
|
||||
"fee": PumpFunAddresses.FEE,
|
||||
"mint": token_info.mint,
|
||||
"bonding_curve": additional_accounts.get("bonding_curve", token_info.bonding_curve),
|
||||
"associated_bonding_curve": additional_accounts.get("associated_bonding_curve", token_info.associated_bonding_curve),
|
||||
"bonding_curve": additional_accounts.get(
|
||||
"bonding_curve", token_info.bonding_curve
|
||||
),
|
||||
"associated_bonding_curve": additional_accounts.get(
|
||||
"associated_bonding_curve", token_info.associated_bonding_curve
|
||||
),
|
||||
"user_token_account": self.derive_user_token_account(user, token_info.mint),
|
||||
"user": user,
|
||||
"system_program": SystemAddresses.SYSTEM_PROGRAM,
|
||||
"token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault),
|
||||
"creator_vault": additional_accounts.get(
|
||||
"creator_vault", token_info.creator_vault
|
||||
),
|
||||
"event_authority": PumpFunAddresses.EVENT_AUTHORITY,
|
||||
"program": PumpFunAddresses.PROGRAM,
|
||||
"global_volume_accumulator": self.derive_global_volume_accumulator(),
|
||||
"user_volume_accumulator": self.derive_user_volume_accumulator(user),
|
||||
}
|
||||
|
||||
def get_sell_instruction_accounts(self, token_info: TokenInfo, user: Pubkey) -> dict[str, Pubkey]:
|
||||
|
||||
def get_sell_instruction_accounts(
|
||||
self, token_info: TokenInfo, user: Pubkey
|
||||
) -> dict[str, Pubkey]:
|
||||
"""Get all accounts needed for a sell instruction.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary of account addresses for sell instruction
|
||||
"""
|
||||
additional_accounts = self.get_additional_accounts(token_info)
|
||||
|
||||
|
||||
return {
|
||||
"global": PumpFunAddresses.GLOBAL,
|
||||
"fee": PumpFunAddresses.FEE,
|
||||
"mint": token_info.mint,
|
||||
"bonding_curve": additional_accounts.get("bonding_curve", token_info.bonding_curve),
|
||||
"associated_bonding_curve": additional_accounts.get("associated_bonding_curve", token_info.associated_bonding_curve),
|
||||
"bonding_curve": additional_accounts.get(
|
||||
"bonding_curve", token_info.bonding_curve
|
||||
),
|
||||
"associated_bonding_curve": additional_accounts.get(
|
||||
"associated_bonding_curve", token_info.associated_bonding_curve
|
||||
),
|
||||
"user_token_account": self.derive_user_token_account(user, token_info.mint),
|
||||
"user": user,
|
||||
"system_program": SystemAddresses.SYSTEM_PROGRAM,
|
||||
"creator_vault": additional_accounts.get("creator_vault", token_info.creator_vault),
|
||||
"creator_vault": additional_accounts.get(
|
||||
"creator_vault", token_info.creator_vault
|
||||
),
|
||||
"token_program": SystemAddresses.TOKEN_PROGRAM,
|
||||
"event_authority": PumpFunAddresses.EVENT_AUTHORITY,
|
||||
"program": PumpFunAddresses.PROGRAM,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,30 +20,30 @@ logger = get_logger(__name__)
|
||||
|
||||
class PumpFunCurveManager(CurveManager):
|
||||
"""Pump.Fun implementation of CurveManager interface using IDL-based decoding."""
|
||||
|
||||
|
||||
def __init__(self, client: SolanaClient, idl_parser: IDLParser):
|
||||
"""Initialize pump.fun curve manager with injected IDL parser.
|
||||
|
||||
|
||||
Args:
|
||||
client: Solana RPC client
|
||||
idl_parser: Pre-loaded IDL parser for pump.fun platform
|
||||
"""
|
||||
self.client = client
|
||||
self._idl_parser = idl_parser
|
||||
|
||||
|
||||
logger.info("Pump.Fun curve manager initialized with injected IDL parser")
|
||||
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this manager serves."""
|
||||
return Platform.PUMP_FUN
|
||||
|
||||
|
||||
async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get the current state of a pump.fun bonding curve.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary containing bonding curve state data
|
||||
"""
|
||||
@@ -51,140 +51,139 @@ class PumpFunCurveManager(CurveManager):
|
||||
account = await self.client.get_account_info(pool_address)
|
||||
if not account.data:
|
||||
raise ValueError(f"No data in bonding curve account {pool_address}")
|
||||
|
||||
|
||||
# Decode bonding curve state using injected IDL parser
|
||||
curve_state_data = self._decode_curve_state_with_idl(account.data)
|
||||
|
||||
|
||||
return curve_state_data
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Failed to get curve state")
|
||||
raise ValueError(f"Invalid bonding curve state: {e!s}")
|
||||
|
||||
|
||||
async def calculate_price(self, pool_address: Pubkey) -> float:
|
||||
"""Calculate current token price from bonding curve state.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
|
||||
Returns:
|
||||
Current token price in SOL
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
|
||||
# Use virtual reserves for price calculation
|
||||
virtual_token_reserves = pool_state["virtual_token_reserves"]
|
||||
virtual_sol_reserves = pool_state["virtual_sol_reserves"]
|
||||
|
||||
|
||||
if virtual_token_reserves <= 0:
|
||||
return 0.0
|
||||
|
||||
|
||||
# Price = sol_reserves / token_reserves
|
||||
price_lamports = virtual_sol_reserves / virtual_token_reserves
|
||||
return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
|
||||
|
||||
|
||||
async def calculate_buy_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
self, pool_address: Pubkey, amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected tokens received for a buy operation.
|
||||
|
||||
|
||||
Uses the pump.fun bonding curve formula to calculate token output.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
amount_in: Amount of SOL to spend (in lamports)
|
||||
|
||||
|
||||
Returns:
|
||||
Expected amount of tokens to receive (in raw token units)
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
|
||||
virtual_token_reserves = pool_state["virtual_token_reserves"]
|
||||
virtual_sol_reserves = pool_state["virtual_sol_reserves"]
|
||||
|
||||
|
||||
# Use virtual reserves for bonding curve calculation
|
||||
# Formula: tokens_out = (amount_in * virtual_token_reserves) / (virtual_sol_reserves + amount_in)
|
||||
numerator = amount_in * virtual_token_reserves
|
||||
denominator = virtual_sol_reserves + amount_in
|
||||
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
|
||||
tokens_out = numerator // denominator
|
||||
return tokens_out
|
||||
|
||||
|
||||
async def calculate_sell_amount_out(
|
||||
self,
|
||||
pool_address: Pubkey,
|
||||
amount_in: int
|
||||
self, pool_address: Pubkey, amount_in: int
|
||||
) -> int:
|
||||
"""Calculate expected SOL received for a sell operation.
|
||||
|
||||
|
||||
Uses the pump.fun bonding curve formula to calculate SOL output.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
amount_in: Amount of tokens to sell (in raw token units)
|
||||
|
||||
|
||||
Returns:
|
||||
Expected amount of SOL to receive (in lamports)
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
|
||||
virtual_token_reserves = pool_state["virtual_token_reserves"]
|
||||
virtual_sol_reserves = pool_state["virtual_sol_reserves"]
|
||||
|
||||
|
||||
# Use virtual reserves for bonding curve calculation
|
||||
# Formula: sol_out = (amount_in * virtual_sol_reserves) / (virtual_token_reserves + amount_in)
|
||||
numerator = amount_in * virtual_sol_reserves
|
||||
denominator = virtual_token_reserves + amount_in
|
||||
|
||||
|
||||
if denominator == 0:
|
||||
return 0
|
||||
|
||||
|
||||
sol_out = numerator // denominator
|
||||
return sol_out
|
||||
|
||||
|
||||
async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]:
|
||||
"""Get current bonding curve reserves.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
|
||||
Returns:
|
||||
Tuple of (token_reserves, sol_reserves) in raw units
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
return (pool_state["virtual_token_reserves"], pool_state["virtual_sol_reserves"])
|
||||
|
||||
return (
|
||||
pool_state["virtual_token_reserves"],
|
||||
pool_state["virtual_sol_reserves"],
|
||||
)
|
||||
|
||||
def _decode_curve_state_with_idl(self, data: bytes) -> dict[str, Any]:
|
||||
"""Decode bonding curve state data using injected IDL parser.
|
||||
|
||||
|
||||
Args:
|
||||
data: Raw account data
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary with decoded bonding curve state
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If IDL parsing fails
|
||||
"""
|
||||
# Use injected IDL parser to decode BondingCurve account data
|
||||
decoded_curve_state = self._idl_parser.decode_account_data(
|
||||
data,
|
||||
"BondingCurve",
|
||||
skip_discriminator=True
|
||||
data, "BondingCurve", skip_discriminator=True
|
||||
)
|
||||
|
||||
|
||||
if not decoded_curve_state:
|
||||
raise ValueError("Failed to decode bonding curve state with IDL parser")
|
||||
|
||||
|
||||
# Extract the fields we need for trading calculations
|
||||
# Based on the BondingCurve structure from the IDL
|
||||
curve_data = {
|
||||
"virtual_token_reserves": decoded_curve_state.get("virtual_token_reserves", 0),
|
||||
"virtual_token_reserves": decoded_curve_state.get(
|
||||
"virtual_token_reserves", 0
|
||||
),
|
||||
"virtual_sol_reserves": decoded_curve_state.get("virtual_sol_reserves", 0),
|
||||
"real_token_reserves": decoded_curve_state.get("real_token_reserves", 0),
|
||||
"real_sol_reserves": decoded_curve_state.get("real_sol_reserves", 0),
|
||||
@@ -192,106 +191,121 @@ class PumpFunCurveManager(CurveManager):
|
||||
"complete": decoded_curve_state.get("complete", False),
|
||||
"creator": decoded_curve_state.get("creator", ""),
|
||||
}
|
||||
|
||||
|
||||
# Calculate additional metrics
|
||||
if curve_data["virtual_token_reserves"] > 0:
|
||||
curve_data["price_per_token"] = (
|
||||
(curve_data["virtual_sol_reserves"] / curve_data["virtual_token_reserves"])
|
||||
* (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
|
||||
(
|
||||
curve_data["virtual_sol_reserves"]
|
||||
/ curve_data["virtual_token_reserves"]
|
||||
)
|
||||
* (10**TOKEN_DECIMALS)
|
||||
/ LAMPORTS_PER_SOL
|
||||
)
|
||||
else:
|
||||
curve_data["price_per_token"] = 0
|
||||
|
||||
|
||||
# Add convenience decimal fields
|
||||
curve_data["token_reserves_decimal"] = curve_data["virtual_token_reserves"] / 10**TOKEN_DECIMALS
|
||||
curve_data["sol_reserves_decimal"] = curve_data["virtual_sol_reserves"] / LAMPORTS_PER_SOL
|
||||
|
||||
logger.debug(f"Decoded curve state: virtual_token_reserves={curve_data['virtual_token_reserves']}, "
|
||||
f"virtual_sol_reserves={curve_data['virtual_sol_reserves']}, "
|
||||
f"price={curve_data['price_per_token']:.8f} SOL")
|
||||
|
||||
curve_data["token_reserves_decimal"] = (
|
||||
curve_data["virtual_token_reserves"] / 10**TOKEN_DECIMALS
|
||||
)
|
||||
curve_data["sol_reserves_decimal"] = (
|
||||
curve_data["virtual_sol_reserves"] / LAMPORTS_PER_SOL
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"Decoded curve state: virtual_token_reserves={curve_data['virtual_token_reserves']}, "
|
||||
f"virtual_sol_reserves={curve_data['virtual_sol_reserves']}, "
|
||||
f"price={curve_data['price_per_token']:.8f} SOL"
|
||||
)
|
||||
|
||||
return curve_data
|
||||
|
||||
|
||||
# Additional convenience methods for pump.fun specific operations
|
||||
async def calculate_expected_tokens(self, pool_address: Pubkey, sol_amount: float) -> float:
|
||||
async def calculate_expected_tokens(
|
||||
self, pool_address: Pubkey, sol_amount: float
|
||||
) -> float:
|
||||
"""Calculate the expected token amount for a given SOL input.
|
||||
|
||||
|
||||
This is a convenience method that converts between decimal and raw units.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
sol_amount: Amount of SOL to spend (in decimal SOL)
|
||||
|
||||
|
||||
Returns:
|
||||
Expected token amount (in decimal tokens)
|
||||
"""
|
||||
sol_lamports = int(sol_amount * LAMPORTS_PER_SOL)
|
||||
tokens_raw = await self.calculate_buy_amount_out(pool_address, sol_lamports)
|
||||
return tokens_raw / 10**TOKEN_DECIMALS
|
||||
|
||||
async def calculate_expected_sol(self, pool_address: Pubkey, token_amount: float) -> float:
|
||||
|
||||
async def calculate_expected_sol(
|
||||
self, pool_address: Pubkey, token_amount: float
|
||||
) -> float:
|
||||
"""Calculate the expected SOL amount for a given token input.
|
||||
|
||||
|
||||
This is a convenience method that converts between decimal and raw units.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
token_amount: Amount of tokens to sell (in decimal tokens)
|
||||
|
||||
|
||||
Returns:
|
||||
Expected SOL amount (in decimal SOL)
|
||||
"""
|
||||
tokens_raw = int(token_amount * 10**TOKEN_DECIMALS)
|
||||
sol_lamports = await self.calculate_sell_amount_out(pool_address, tokens_raw)
|
||||
return sol_lamports / LAMPORTS_PER_SOL
|
||||
|
||||
|
||||
async def is_curve_complete(self, pool_address: Pubkey) -> bool:
|
||||
"""Check if the bonding curve is complete (migrated to Raydium).
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
|
||||
Returns:
|
||||
True if curve is complete, False otherwise
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
return pool_state.get("complete", False)
|
||||
|
||||
|
||||
async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||
"""Get bonding curve completion progress information.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary with progress information
|
||||
"""
|
||||
pool_state = await self.get_pool_state(pool_address)
|
||||
|
||||
|
||||
# Calculate progress based on SOL raised vs target
|
||||
# This is approximate since the exact target isn't stored in the curve state
|
||||
sol_raised = pool_state["real_sol_reserves"] / LAMPORTS_PER_SOL
|
||||
|
||||
|
||||
# Estimate progress based on typical pump.fun graduation requirements
|
||||
# (This could be made more accurate with additional on-chain data)
|
||||
estimated_target_sol = 85.0 # Typical pump.fun graduation target
|
||||
progress_percentage = min((sol_raised / estimated_target_sol) * 100, 100.0)
|
||||
|
||||
|
||||
return {
|
||||
"complete": pool_state.get("complete", False),
|
||||
"sol_raised": sol_raised,
|
||||
"estimated_target_sol": estimated_target_sol,
|
||||
"progress_percentage": progress_percentage,
|
||||
"tokens_available": pool_state["virtual_token_reserves"] / 10**TOKEN_DECIMALS,
|
||||
"tokens_available": pool_state["virtual_token_reserves"]
|
||||
/ 10**TOKEN_DECIMALS,
|
||||
"market_cap_sol": sol_raised, # Approximate market cap
|
||||
}
|
||||
|
||||
|
||||
def validate_curve_state_structure(self, pool_address: Pubkey) -> bool:
|
||||
"""Validate that the curve state structure matches IDL expectations.
|
||||
|
||||
|
||||
Args:
|
||||
pool_address: Address of the bonding curve
|
||||
|
||||
|
||||
Returns:
|
||||
True if structure is valid, False otherwise
|
||||
"""
|
||||
@@ -299,24 +313,29 @@ class PumpFunCurveManager(CurveManager):
|
||||
# This would be used during development/testing to ensure
|
||||
# the IDL parsing is working correctly
|
||||
pool_state = self.get_pool_state(pool_address)
|
||||
|
||||
|
||||
required_fields = [
|
||||
"virtual_token_reserves", "virtual_sol_reserves",
|
||||
"real_token_reserves", "real_sol_reserves",
|
||||
"token_total_supply", "complete"
|
||||
"virtual_token_reserves",
|
||||
"virtual_sol_reserves",
|
||||
"real_token_reserves",
|
||||
"real_sol_reserves",
|
||||
"token_total_supply",
|
||||
"complete",
|
||||
]
|
||||
|
||||
|
||||
for field in required_fields:
|
||||
if field not in pool_state:
|
||||
logger.error(f"Missing required field: {field}")
|
||||
return False
|
||||
|
||||
|
||||
if field != "complete" and not isinstance(pool_state[field], int):
|
||||
logger.error(f"Field {field} is not an integer: {type(pool_state[field])}")
|
||||
logger.error(
|
||||
f"Field {field} is not an integer: {type(pool_state[field])}"
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
except Exception:
|
||||
logger.exception("Curve state validation failed")
|
||||
return False
|
||||
return False
|
||||
|
||||
@@ -27,40 +27,50 @@ class PumpFunEventParser(EventParser):
|
||||
|
||||
def __init__(self, idl_parser: IDLParser):
|
||||
"""Initialize pump.fun event parser with injected IDL parser.
|
||||
|
||||
|
||||
Args:
|
||||
idl_parser: Pre-loaded IDL parser for pump.fun platform
|
||||
"""
|
||||
self._idl_parser = idl_parser
|
||||
|
||||
|
||||
event_discriminators = self._idl_parser.get_event_discriminators()
|
||||
self._create_event_discriminator_bytes = event_discriminators["CreateEvent"]
|
||||
self._create_event_discriminator = struct.unpack("<Q", self._create_event_discriminator_bytes)[0]
|
||||
|
||||
self._create_event_discriminator = struct.unpack(
|
||||
"<Q", self._create_event_discriminator_bytes
|
||||
)[0]
|
||||
|
||||
instruction_discriminators = self._idl_parser.get_instruction_discriminators()
|
||||
self._create_instruction_discriminator_bytes = instruction_discriminators["create"]
|
||||
self._create_instruction_discriminator = struct.unpack("<Q", self._create_instruction_discriminator_bytes)[0]
|
||||
|
||||
logger.info("Pump.Fun event parser initialized with IDL-based event and instruction parsing")
|
||||
logger.info(f"CreateEvent discriminator: {self._create_event_discriminator_bytes.hex()}")
|
||||
logger.info(f"create instruction discriminator: {self._create_instruction_discriminator_bytes.hex()}")
|
||||
|
||||
self._create_instruction_discriminator_bytes = instruction_discriminators[
|
||||
"create"
|
||||
]
|
||||
self._create_instruction_discriminator = struct.unpack(
|
||||
"<Q", self._create_instruction_discriminator_bytes
|
||||
)[0]
|
||||
|
||||
logger.info(
|
||||
"Pump.Fun event parser initialized with IDL-based event and instruction parsing"
|
||||
)
|
||||
logger.info(
|
||||
f"CreateEvent discriminator: {self._create_event_discriminator_bytes.hex()}"
|
||||
)
|
||||
logger.info(
|
||||
f"create instruction discriminator: {self._create_instruction_discriminator_bytes.hex()}"
|
||||
)
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this parser serves."""
|
||||
return Platform.PUMP_FUN
|
||||
|
||||
|
||||
def parse_token_creation_from_logs(
|
||||
self,
|
||||
logs: list[str],
|
||||
signature: str
|
||||
self, logs: list[str], signature: str
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from pump.fun transaction logs using IDL event parsing.
|
||||
|
||||
|
||||
Args:
|
||||
logs: List of log strings from transaction
|
||||
signature: Transaction signature
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
@@ -73,13 +83,13 @@ class PumpFunEventParser(EventParser):
|
||||
return None
|
||||
|
||||
logger.info(f"🔍 Parsing token creation from logs for signature: {signature}")
|
||||
|
||||
|
||||
# Look for event data in the logs (CreateEvent data!)
|
||||
# We need to find the Program data that comes after "Instruction: Create"
|
||||
try:
|
||||
create_instruction_found = False
|
||||
program_data_entries = []
|
||||
|
||||
|
||||
# First, collect all Program data entries and note when Create instruction happens
|
||||
for i, log in enumerate(logs):
|
||||
if "Program log: Instruction: Create" in log:
|
||||
@@ -89,87 +99,141 @@ class PumpFunEventParser(EventParser):
|
||||
# Extract base64 encoded event data
|
||||
encoded_data = log.split("Program data: ")[1].strip()
|
||||
program_data_entries.append((i, encoded_data, log))
|
||||
logger.info(f"📊 Found Program data at log index {i}, length: {len(encoded_data)}")
|
||||
|
||||
logger.info(
|
||||
f"📊 Found Program data at log index {i}, length: {len(encoded_data)}"
|
||||
)
|
||||
|
||||
if not create_instruction_found:
|
||||
logger.info("❌ No Create instruction found in logs")
|
||||
return None
|
||||
|
||||
|
||||
if not program_data_entries:
|
||||
logger.info("❌ No Program data entries found in logs")
|
||||
return None
|
||||
|
||||
logger.info(f"🔍 Found {len(program_data_entries)} Program data entries to check")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"🔍 Found {len(program_data_entries)} Program data entries to check"
|
||||
)
|
||||
|
||||
# Try each Program data entry
|
||||
for entry_idx, (log_idx, encoded_data, full_log) in enumerate(program_data_entries):
|
||||
for entry_idx, (log_idx, encoded_data, full_log) in enumerate(
|
||||
program_data_entries
|
||||
):
|
||||
try:
|
||||
logger.info(f"🧪 Trying Program data entry {entry_idx + 1}/{len(program_data_entries)} (log index {log_idx})")
|
||||
|
||||
logger.info(
|
||||
f"🧪 Trying Program data entry {entry_idx + 1}/{len(program_data_entries)} (log index {log_idx})"
|
||||
)
|
||||
|
||||
decoded_data = base64.b64decode(encoded_data)
|
||||
|
||||
|
||||
if len(decoded_data) < 8:
|
||||
logger.info(f"⚠️ Program data too short: {len(decoded_data)} bytes")
|
||||
logger.info(
|
||||
f"⚠️ Program data too short: {len(decoded_data)} bytes"
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
# Check discriminator from program data
|
||||
discriminator = decoded_data[:8]
|
||||
discriminator_int = struct.unpack("<Q", discriminator)[0]
|
||||
|
||||
logger.info(f"🔍 Program data discriminator: {discriminator.hex()} (int: {discriminator_int})")
|
||||
logger.info(f"🎯 Expected CreateEvent discriminator: {self._create_event_discriminator_bytes.hex()} (int: {self._create_event_discriminator})")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"🔍 Program data discriminator: {discriminator.hex()} (int: {discriminator_int})"
|
||||
)
|
||||
logger.info(
|
||||
f"🎯 Expected CreateEvent discriminator: {self._create_event_discriminator_bytes.hex()} (int: {self._create_event_discriminator})"
|
||||
)
|
||||
|
||||
# Try to decode as CreateEvent using IDL parser
|
||||
decoded_event = self._idl_parser.decode_event_data(decoded_data, "CreateEvent")
|
||||
|
||||
decoded_event = self._idl_parser.decode_event_data(
|
||||
decoded_data, "CreateEvent"
|
||||
)
|
||||
|
||||
if not decoded_event:
|
||||
logger.info("❌ IDL parser returned None for CreateEvent")
|
||||
continue
|
||||
|
||||
if decoded_event.get('event_name') != 'CreateEvent':
|
||||
logger.info(f"❌ Wrong event type: {decoded_event.get('event_name', 'None')}")
|
||||
|
||||
if decoded_event.get("event_name") != "CreateEvent":
|
||||
logger.info(
|
||||
f"❌ Wrong event type: {decoded_event.get('event_name', 'None')}"
|
||||
)
|
||||
continue
|
||||
|
||||
logger.info(f"✅ Successfully decoded event: {decoded_event.get('event_name', 'Unknown')}")
|
||||
logger.info(f"🔍 Event fields: {list(decoded_event.get('fields', {}).keys())}")
|
||||
logger.info(
|
||||
f"✅ Successfully decoded event: {decoded_event.get('event_name', 'Unknown')}"
|
||||
)
|
||||
logger.info(
|
||||
f"🔍 Event fields: {list(decoded_event.get('fields', {}).keys())}"
|
||||
)
|
||||
|
||||
fields = decoded_event.get('fields', {})
|
||||
fields = decoded_event.get("fields", {})
|
||||
if not fields:
|
||||
logger.info("❌ No fields found in decoded event")
|
||||
continue
|
||||
|
||||
|
||||
# Validate required fields exist
|
||||
required_fields = ["mint", "bonding_curve", "user", "creator", "name", "symbol", "uri"]
|
||||
missing_fields = [field for field in required_fields if field not in fields]
|
||||
required_fields = [
|
||||
"mint",
|
||||
"bonding_curve",
|
||||
"user",
|
||||
"creator",
|
||||
"name",
|
||||
"symbol",
|
||||
"uri",
|
||||
]
|
||||
missing_fields = [
|
||||
field for field in required_fields if field not in fields
|
||||
]
|
||||
if missing_fields:
|
||||
logger.info(f"❌ Missing required fields: {missing_fields}")
|
||||
continue
|
||||
|
||||
logger.info(f"🎯 Token found: {fields.get('symbol', 'Unknown')} ({fields.get('name', 'Unknown')})")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"🎯 Token found: {fields.get('symbol', 'Unknown')} ({fields.get('name', 'Unknown')})"
|
||||
)
|
||||
|
||||
# Convert string representations to Pubkey objects
|
||||
# Note: IDL parser returns pubkeys as base58 strings
|
||||
try:
|
||||
mint = Pubkey.from_string(fields["mint"]) if isinstance(fields["mint"], str) else fields["mint"]
|
||||
bonding_curve = Pubkey.from_string(fields["bonding_curve"]) if isinstance(fields["bonding_curve"], str) else fields["bonding_curve"]
|
||||
user = Pubkey.from_string(fields["user"]) if isinstance(fields["user"], str) else fields["user"]
|
||||
creator = Pubkey.from_string(fields["creator"]) if isinstance(fields["creator"], str) else fields["creator"]
|
||||
|
||||
mint = (
|
||||
Pubkey.from_string(fields["mint"])
|
||||
if isinstance(fields["mint"], str)
|
||||
else fields["mint"]
|
||||
)
|
||||
bonding_curve = (
|
||||
Pubkey.from_string(fields["bonding_curve"])
|
||||
if isinstance(fields["bonding_curve"], str)
|
||||
else fields["bonding_curve"]
|
||||
)
|
||||
user = (
|
||||
Pubkey.from_string(fields["user"])
|
||||
if isinstance(fields["user"], str)
|
||||
else fields["user"]
|
||||
)
|
||||
creator = (
|
||||
Pubkey.from_string(fields["creator"])
|
||||
if isinstance(fields["creator"], str)
|
||||
else fields["creator"]
|
||||
)
|
||||
|
||||
logger.info(f"🔑 Mint: {mint}")
|
||||
logger.info(f"🔑 Bonding Curve: {bonding_curve}")
|
||||
logger.info(f"🔑 User: {user}")
|
||||
logger.info(f"🔑 Creator: {creator}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"❌ Failed to convert pubkey fields: {e}")
|
||||
continue
|
||||
|
||||
|
||||
# Derive additional addresses
|
||||
associated_bonding_curve = self._derive_associated_bonding_curve(mint, bonding_curve)
|
||||
associated_bonding_curve = self._derive_associated_bonding_curve(
|
||||
mint, bonding_curve
|
||||
)
|
||||
creator_vault = self._derive_creator_vault(creator)
|
||||
|
||||
logger.info(f"✅ Successfully parsed CreateEvent for token: {fields.get('symbol', 'Unknown')}")
|
||||
|
||||
|
||||
logger.info(
|
||||
f"✅ Successfully parsed CreateEvent for token: {fields.get('symbol', 'Unknown')}"
|
||||
)
|
||||
|
||||
return TokenInfo(
|
||||
name=fields["name"],
|
||||
symbol=fields["symbol"],
|
||||
@@ -183,35 +247,36 @@ class PumpFunEventParser(EventParser):
|
||||
creator_vault=creator_vault,
|
||||
creation_timestamp=monotonic(),
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.info(f"❌ Failed to decode Program data entry {entry_idx + 1}: {e}")
|
||||
logger.info(
|
||||
f"❌ Failed to decode Program data entry {entry_idx + 1}: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
|
||||
logger.info("❌ No valid CreateEvent found in any Program data entries")
|
||||
return None
|
||||
|
||||
|
||||
except Exception:
|
||||
logger.exception("Failed to parse token creation from logs")
|
||||
return None
|
||||
|
||||
|
||||
def parse_token_creation_from_instruction(
|
||||
self,
|
||||
instruction_data: bytes,
|
||||
accounts: list[int],
|
||||
account_keys: list[bytes]
|
||||
self, instruction_data: bytes, accounts: list[int], account_keys: list[bytes]
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from pump.fun instruction data using injected IDL parser.
|
||||
|
||||
|
||||
Args:
|
||||
instruction_data: Raw instruction data
|
||||
accounts: List of account indices
|
||||
account_keys: List of account public keys
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
if not instruction_data.startswith(self._create_instruction_discriminator_bytes):
|
||||
if not instruction_data.startswith(
|
||||
self._create_instruction_discriminator_bytes
|
||||
):
|
||||
return None
|
||||
|
||||
try:
|
||||
@@ -225,12 +290,14 @@ class PumpFunEventParser(EventParser):
|
||||
return Pubkey.from_bytes(account_keys[account_index])
|
||||
|
||||
# Parse instruction data using injected IDL parser
|
||||
decoded = self._idl_parser.decode_instruction(instruction_data, account_keys, accounts)
|
||||
if not decoded or decoded['instruction_name'] != 'create':
|
||||
decoded = self._idl_parser.decode_instruction(
|
||||
instruction_data, account_keys, accounts
|
||||
)
|
||||
if not decoded or decoded["instruction_name"] != "create":
|
||||
return None
|
||||
|
||||
args = decoded.get('args', {})
|
||||
|
||||
|
||||
args = decoded.get("args", {})
|
||||
|
||||
# Extract account information based on IDL account order
|
||||
mint = get_account_key(0)
|
||||
bonding_curve = get_account_key(2)
|
||||
@@ -241,7 +308,11 @@ class PumpFunEventParser(EventParser):
|
||||
return None
|
||||
|
||||
# Create creator vault
|
||||
creator = Pubkey.from_string(args.get("creator", str(user))) if args.get("creator") else user
|
||||
creator = (
|
||||
Pubkey.from_string(args.get("creator", str(user)))
|
||||
if args.get("creator")
|
||||
else user
|
||||
)
|
||||
creator_vault = self._derive_creator_vault(creator)
|
||||
|
||||
return TokenInfo(
|
||||
@@ -261,21 +332,20 @@ class PumpFunEventParser(EventParser):
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse create instruction: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def parse_token_creation_from_geyser(
|
||||
self,
|
||||
transaction_info: Any
|
||||
self, transaction_info: Any
|
||||
) -> TokenInfo | None:
|
||||
"""Parse token creation from Geyser transaction data.
|
||||
|
||||
|
||||
Args:
|
||||
transaction_info: Geyser transaction information
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
try:
|
||||
if not hasattr(transaction_info, 'transaction'):
|
||||
if not hasattr(transaction_info, "transaction"):
|
||||
return None
|
||||
|
||||
tx = transaction_info.transaction.transaction.transaction
|
||||
@@ -304,37 +374,37 @@ class PumpFunEventParser(EventParser):
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse geyser transaction: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_program_id(self) -> Pubkey:
|
||||
"""Get the pump.fun program ID this parser monitors.
|
||||
|
||||
|
||||
Returns:
|
||||
Pump.fun program ID
|
||||
"""
|
||||
return PumpFunAddresses.PROGRAM
|
||||
|
||||
|
||||
def get_instruction_discriminators(self) -> list[bytes]:
|
||||
"""Get instruction discriminators for token creation.
|
||||
|
||||
|
||||
Returns:
|
||||
List of discriminator bytes to match
|
||||
"""
|
||||
return [self._create_instruction_discriminator_bytes]
|
||||
|
||||
|
||||
def get_event_discriminators(self) -> list[bytes]:
|
||||
"""Get event discriminators for token creation.
|
||||
|
||||
|
||||
Returns:
|
||||
List of event discriminator bytes to match
|
||||
"""
|
||||
return [self._create_event_discriminator_bytes]
|
||||
|
||||
|
||||
def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
|
||||
"""Parse token creation from block data (for block listener).
|
||||
|
||||
|
||||
Args:
|
||||
block_data: Block data from WebSocket
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
@@ -353,21 +423,26 @@ class PumpFunEventParser(EventParser):
|
||||
tx_data_encoded = tx_data[0]
|
||||
tx_data_decoded = base64.b64decode(tx_data_encoded)
|
||||
transaction = VersionedTransaction.from_bytes(tx_data_decoded)
|
||||
|
||||
|
||||
for ix in transaction.message.instructions:
|
||||
program_id = transaction.message.account_keys[ix.program_id_index]
|
||||
|
||||
program_id = transaction.message.account_keys[
|
||||
ix.program_id_index
|
||||
]
|
||||
|
||||
# Check if instruction is from pump.fun program
|
||||
if str(program_id) != str(self.get_program_id()):
|
||||
continue
|
||||
|
||||
ix_data = bytes(ix.data)
|
||||
|
||||
|
||||
# Check for create discriminator
|
||||
if len(ix_data) >= 8:
|
||||
discriminator = struct.unpack("<Q", ix_data[:8])[0]
|
||||
|
||||
if discriminator == self._create_instruction_discriminator:
|
||||
|
||||
if (
|
||||
discriminator
|
||||
== self._create_instruction_discriminator
|
||||
):
|
||||
# Token creation should have substantial data and many accounts
|
||||
if len(ix_data) <= 8 or len(ix.accounts) < 10:
|
||||
continue
|
||||
@@ -376,59 +451,73 @@ class PumpFunEventParser(EventParser):
|
||||
bytes(key)
|
||||
for key in transaction.message.account_keys
|
||||
]
|
||||
|
||||
|
||||
# Parse the instruction
|
||||
token_info = self.parse_token_creation_from_instruction(
|
||||
ix_data, ix.accounts, account_keys_bytes
|
||||
token_info = (
|
||||
self.parse_token_creation_from_instruction(
|
||||
ix_data, ix.accounts, account_keys_bytes
|
||||
)
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse block transaction: {e}")
|
||||
continue
|
||||
|
||||
|
||||
# Handle already decoded transaction data
|
||||
elif isinstance(tx_data, dict) and "message" in tx_data:
|
||||
try:
|
||||
message = tx_data["message"]
|
||||
if "instructions" not in message or "accountKeys" not in message:
|
||||
if (
|
||||
"instructions" not in message
|
||||
or "accountKeys" not in message
|
||||
):
|
||||
continue
|
||||
|
||||
|
||||
for ix in message["instructions"]:
|
||||
if "programIdIndex" not in ix or "accounts" not in ix or "data" not in ix:
|
||||
if (
|
||||
"programIdIndex" not in ix
|
||||
or "accounts" not in ix
|
||||
or "data" not in ix
|
||||
):
|
||||
continue
|
||||
|
||||
|
||||
program_idx = ix["programIdIndex"]
|
||||
if program_idx >= len(message["accountKeys"]):
|
||||
continue
|
||||
|
||||
|
||||
program_id_str = message["accountKeys"][program_idx]
|
||||
if program_id_str != str(self.get_program_id()):
|
||||
continue
|
||||
|
||||
|
||||
# Decode instruction data
|
||||
ix_data = base64.b64decode(ix["data"])
|
||||
|
||||
|
||||
if len(ix_data) >= 8:
|
||||
discriminator = struct.unpack("<Q", ix_data[:8])[0]
|
||||
|
||||
if discriminator == self._create_instruction_discriminator:
|
||||
|
||||
if (
|
||||
discriminator
|
||||
== self._create_instruction_discriminator
|
||||
):
|
||||
if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
|
||||
continue
|
||||
|
||||
|
||||
# Convert account keys to bytes for parsing
|
||||
account_keys_bytes = [
|
||||
Pubkey.from_string(key).to_bytes()
|
||||
Pubkey.from_string(key).to_bytes()
|
||||
for key in message["accountKeys"]
|
||||
]
|
||||
|
||||
token_info = self.parse_token_creation_from_instruction(
|
||||
ix_data, ix["accounts"], account_keys_bytes
|
||||
|
||||
token_info = (
|
||||
self.parse_token_creation_from_instruction(
|
||||
ix_data, ix["accounts"], account_keys_bytes
|
||||
)
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse decoded block transaction: {e}")
|
||||
continue
|
||||
@@ -438,13 +527,13 @@ class PumpFunEventParser(EventParser):
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to parse block data: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _derive_creator_vault(self, creator: Pubkey) -> Pubkey:
|
||||
"""Derive the creator vault for a creator.
|
||||
|
||||
|
||||
Args:
|
||||
creator: Creator address
|
||||
|
||||
|
||||
Returns:
|
||||
Creator vault address
|
||||
"""
|
||||
@@ -453,14 +542,16 @@ class PumpFunEventParser(EventParser):
|
||||
PumpFunAddresses.PROGRAM,
|
||||
)
|
||||
return derived_address
|
||||
|
||||
def _derive_associated_bonding_curve(self, mint: Pubkey, bonding_curve: Pubkey) -> Pubkey:
|
||||
|
||||
def _derive_associated_bonding_curve(
|
||||
self, mint: Pubkey, bonding_curve: Pubkey
|
||||
) -> Pubkey:
|
||||
"""Derive the associated bonding curve (ATA of bonding curve for the token).
|
||||
|
||||
|
||||
Args:
|
||||
mint: Token mint address
|
||||
bonding_curve: Bonding curve address
|
||||
|
||||
|
||||
Returns:
|
||||
Associated bonding curve address
|
||||
"""
|
||||
@@ -474,12 +565,12 @@ class PumpFunEventParser(EventParser):
|
||||
)
|
||||
return derived_address
|
||||
|
||||
@property
|
||||
@property
|
||||
def verbose(self) -> bool:
|
||||
"""Check if verbose logging is enabled."""
|
||||
return getattr(self, '_verbose', False)
|
||||
|
||||
return getattr(self, "_verbose", False)
|
||||
|
||||
@verbose.setter
|
||||
def verbose(self, value: bool) -> None:
|
||||
"""Set verbose logging."""
|
||||
self._verbose = value
|
||||
self._verbose = value
|
||||
|
||||
@@ -21,52 +21,52 @@ logger = get_logger(__name__)
|
||||
|
||||
class PumpFunInstructionBuilder(InstructionBuilder):
|
||||
"""Pump.Fun implementation of InstructionBuilder interface with IDL-based discriminators."""
|
||||
|
||||
|
||||
def __init__(self, idl_parser: IDLParser):
|
||||
"""Initialize pump.fun instruction builder with injected IDL parser.
|
||||
|
||||
|
||||
Args:
|
||||
idl_parser: Pre-loaded IDL parser for pump.fun platform
|
||||
"""
|
||||
self._idl_parser = idl_parser
|
||||
|
||||
|
||||
# Get discriminators from injected IDL parser
|
||||
discriminators = self._idl_parser.get_instruction_discriminators()
|
||||
self._buy_discriminator = discriminators["buy"]
|
||||
self._sell_discriminator = discriminators["sell"]
|
||||
|
||||
|
||||
logger.info("Pump.Fun instruction builder initialized with injected IDL parser")
|
||||
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this builder serves."""
|
||||
return Platform.PUMP_FUN
|
||||
|
||||
|
||||
async def build_buy_instruction(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
address_provider: AddressProvider,
|
||||
) -> list[Instruction]:
|
||||
"""Build buy instruction(s) for pump.fun.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of SOL to spend (in lamports)
|
||||
minimum_amount_out: Minimum tokens expected (raw token units)
|
||||
address_provider: Platform address provider
|
||||
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the buy operation
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
|
||||
# Get all required accounts
|
||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||
|
||||
|
||||
# 1. Create idempotent ATA instruction (won't fail if ATA already exists)
|
||||
ata_instruction = create_idempotent_associated_token_account(
|
||||
user, # payer
|
||||
@@ -75,116 +75,181 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
||||
SystemAddresses.TOKEN_PROGRAM, # token program
|
||||
)
|
||||
instructions.append(ata_instruction)
|
||||
|
||||
|
||||
# 2. Build buy instruction
|
||||
buy_accounts = [
|
||||
AccountMeta(pubkey=accounts_info["global"], is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["global"], is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(pubkey=accounts_info["fee"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["associated_bonding_curve"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user_token_account"], is_signer=False, is_writable=True),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["mint"], is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["associated_bonding_curve"],
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["user_token_account"],
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
),
|
||||
AccountMeta(pubkey=accounts_info["user"], is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["system_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["global_volume_accumulator"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user_volume_accumulator"], is_signer=False, is_writable=True),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["system_program"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["token_program"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["event_authority"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["program"], is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["global_volume_accumulator"],
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["user_volume_accumulator"],
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# Build instruction data: discriminator + token_amount + max_sol_cost
|
||||
instruction_data = (
|
||||
self._buy_discriminator +
|
||||
struct.pack("<Q", minimum_amount_out) + # token amount in raw units
|
||||
struct.pack("<Q", amount_in) # max SOL cost in lamports
|
||||
self._buy_discriminator
|
||||
+ struct.pack("<Q", minimum_amount_out) # token amount in raw units
|
||||
+ struct.pack("<Q", amount_in) # max SOL cost in lamports
|
||||
)
|
||||
|
||||
|
||||
buy_instruction = Instruction(
|
||||
program_id=accounts_info["program"],
|
||||
data=instruction_data,
|
||||
accounts=buy_accounts
|
||||
accounts=buy_accounts,
|
||||
)
|
||||
instructions.append(buy_instruction)
|
||||
|
||||
|
||||
return instructions
|
||||
|
||||
|
||||
async def build_sell_instruction(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
amount_in: int,
|
||||
minimum_amount_out: int,
|
||||
address_provider: AddressProvider
|
||||
address_provider: AddressProvider,
|
||||
) -> list[Instruction]:
|
||||
"""Build sell instruction(s) for pump.fun.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
amount_in: Amount of tokens to sell (raw token units)
|
||||
minimum_amount_out: Minimum SOL expected (in lamports)
|
||||
address_provider: Platform address provider
|
||||
|
||||
|
||||
Returns:
|
||||
List of instructions needed for the sell operation
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
|
||||
# Get all required accounts
|
||||
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
||||
|
||||
|
||||
# Build sell instruction accounts
|
||||
sell_accounts = [
|
||||
AccountMeta(pubkey=accounts_info["global"], is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["global"], is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(pubkey=accounts_info["fee"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["mint"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["associated_bonding_curve"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["user_token_account"], is_signer=False, is_writable=True),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["mint"], is_signer=False, is_writable=False
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["bonding_curve"], is_signer=False, is_writable=True
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["associated_bonding_curve"],
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["user_token_account"],
|
||||
is_signer=False,
|
||||
is_writable=True,
|
||||
),
|
||||
AccountMeta(pubkey=accounts_info["user"], is_signer=True, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["system_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True),
|
||||
AccountMeta(pubkey=accounts_info["token_program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["event_authority"], is_signer=False, is_writable=False),
|
||||
AccountMeta(pubkey=accounts_info["program"], is_signer=False, is_writable=False),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["system_program"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["creator_vault"], is_signer=False, is_writable=True
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["token_program"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["event_authority"],
|
||||
is_signer=False,
|
||||
is_writable=False,
|
||||
),
|
||||
AccountMeta(
|
||||
pubkey=accounts_info["program"], is_signer=False, is_writable=False
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# Build instruction data: discriminator + token_amount + min_sol_output
|
||||
instruction_data = (
|
||||
self._sell_discriminator +
|
||||
struct.pack("<Q", amount_in) + # token amount in raw units
|
||||
struct.pack("<Q", minimum_amount_out) # min SOL output in lamports
|
||||
self._sell_discriminator
|
||||
+ struct.pack("<Q", amount_in) # token amount in raw units
|
||||
+ struct.pack("<Q", minimum_amount_out) # min SOL output in lamports
|
||||
)
|
||||
|
||||
|
||||
sell_instruction = Instruction(
|
||||
program_id=accounts_info["program"],
|
||||
data=instruction_data,
|
||||
accounts=sell_accounts
|
||||
accounts=sell_accounts,
|
||||
)
|
||||
instructions.append(sell_instruction)
|
||||
|
||||
|
||||
return instructions
|
||||
|
||||
|
||||
def get_required_accounts_for_buy(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for buy operation (for priority fee calculation).
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
accounts_info = address_provider.get_buy_instruction_accounts(token_info, user)
|
||||
|
||||
|
||||
return [
|
||||
accounts_info["mint"],
|
||||
accounts_info["bonding_curve"],
|
||||
@@ -196,25 +261,22 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
||||
accounts_info["user_volume_accumulator"],
|
||||
accounts_info["program"],
|
||||
]
|
||||
|
||||
|
||||
def get_required_accounts_for_sell(
|
||||
self,
|
||||
token_info: TokenInfo,
|
||||
user: Pubkey,
|
||||
address_provider: AddressProvider
|
||||
self, token_info: TokenInfo, user: Pubkey, address_provider: AddressProvider
|
||||
) -> list[Pubkey]:
|
||||
"""Get list of accounts required for sell operation (for priority fee calculation).
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
user: User's wallet address
|
||||
address_provider: Platform address provider
|
||||
|
||||
|
||||
Returns:
|
||||
List of account addresses that will be accessed
|
||||
"""
|
||||
accounts_info = address_provider.get_sell_instruction_accounts(token_info, user)
|
||||
|
||||
|
||||
return [
|
||||
accounts_info["mint"],
|
||||
accounts_info["bonding_curve"],
|
||||
@@ -224,25 +286,25 @@ class PumpFunInstructionBuilder(InstructionBuilder):
|
||||
accounts_info["creator_vault"],
|
||||
accounts_info["program"],
|
||||
]
|
||||
|
||||
|
||||
def calculate_token_amount_raw(self, token_amount_decimal: float) -> int:
|
||||
"""Convert decimal token amount to raw token units.
|
||||
|
||||
|
||||
Args:
|
||||
token_amount_decimal: Token amount in decimal form
|
||||
|
||||
|
||||
Returns:
|
||||
Token amount in raw units (adjusted for decimals)
|
||||
"""
|
||||
return int(token_amount_decimal * 10**TOKEN_DECIMALS)
|
||||
|
||||
|
||||
def calculate_token_amount_decimal(self, token_amount_raw: int) -> float:
|
||||
"""Convert raw token amount to decimal form.
|
||||
|
||||
|
||||
Args:
|
||||
token_amount_raw: Token amount in raw units
|
||||
|
||||
|
||||
Returns:
|
||||
Token amount in decimal form
|
||||
"""
|
||||
return token_amount_raw / 10**TOKEN_DECIMALS
|
||||
return token_amount_raw / 10**TOKEN_DECIMALS
|
||||
|
||||
@@ -14,39 +14,39 @@ logger = get_logger(__name__)
|
||||
|
||||
class PumpFunPumpPortalProcessor:
|
||||
"""PumpPortal processor for pump.fun tokens."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the processor with address provider."""
|
||||
self.address_provider = PumpFunAddressProvider()
|
||||
|
||||
|
||||
@property
|
||||
def platform(self) -> Platform:
|
||||
"""Get the platform this processor handles."""
|
||||
return Platform.PUMP_FUN
|
||||
|
||||
|
||||
@property
|
||||
def supported_pool_names(self) -> list[str]:
|
||||
"""Get the pool names this processor supports from PumpPortal."""
|
||||
return ["pump"] # PumpPortal pool name for pump.fun
|
||||
|
||||
|
||||
def can_process(self, token_data: dict) -> bool:
|
||||
"""Check if this processor can handle the given token data.
|
||||
|
||||
|
||||
Args:
|
||||
token_data: Token data from PumpPortal
|
||||
|
||||
|
||||
Returns:
|
||||
True if this processor can handle the token data
|
||||
"""
|
||||
pool = token_data.get("pool", "").lower()
|
||||
return pool in self.supported_pool_names
|
||||
|
||||
|
||||
def process_token_data(self, token_data: dict) -> TokenInfo | None:
|
||||
"""Process pump.fun token data from PumpPortal.
|
||||
|
||||
|
||||
Args:
|
||||
token_data: Token data from PumpPortal WebSocket
|
||||
|
||||
|
||||
Returns:
|
||||
TokenInfo if token creation found, None otherwise
|
||||
"""
|
||||
@@ -81,8 +81,10 @@ class PumpFunPumpPortalProcessor:
|
||||
creator = user
|
||||
|
||||
# Derive additional addresses using platform provider
|
||||
associated_bonding_curve = self.address_provider.derive_associated_bonding_curve(
|
||||
mint, bonding_curve
|
||||
associated_bonding_curve = (
|
||||
self.address_provider.derive_associated_bonding_curve(
|
||||
mint, bonding_curve
|
||||
)
|
||||
)
|
||||
creator_vault = self.address_provider.derive_creator_vault(creator)
|
||||
|
||||
@@ -101,4 +103,4 @@ class PumpFunPumpPortalProcessor:
|
||||
|
||||
except Exception:
|
||||
logger.exception("Failed to process PumpPortal token data")
|
||||
return None
|
||||
return None
|
||||
|
||||
+78
-68
@@ -19,6 +19,7 @@ from interfaces.core import Platform, TokenInfo
|
||||
@dataclass
|
||||
class TradeResult:
|
||||
"""Enhanced result of a trading operation with platform support."""
|
||||
|
||||
success: bool
|
||||
platform: Platform = Platform.PUMP_FUN # Add platform tracking
|
||||
tx_signature: str | None = None
|
||||
@@ -28,7 +29,7 @@ class TradeResult:
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Convert to dictionary for logging/serialization.
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary representation of the trade result
|
||||
"""
|
||||
@@ -51,7 +52,7 @@ class Trader(ABC):
|
||||
|
||||
Args:
|
||||
token_info: Enhanced token information with platform support
|
||||
|
||||
|
||||
Returns:
|
||||
TradeResult with operation outcome including platform info
|
||||
"""
|
||||
@@ -60,7 +61,7 @@ class Trader(ABC):
|
||||
def _get_relevant_accounts(self, token_info: TokenInfo) -> list[Pubkey]:
|
||||
"""
|
||||
Get the list of accounts relevant for calculating the priority fee.
|
||||
|
||||
|
||||
This is now platform-agnostic and should be overridden by platform-specific traders.
|
||||
|
||||
Args:
|
||||
@@ -71,13 +72,13 @@ class Trader(ABC):
|
||||
"""
|
||||
# Basic implementation - platform-specific traders should override this
|
||||
accounts = [token_info.mint]
|
||||
|
||||
|
||||
if token_info.bonding_curve:
|
||||
accounts.append(token_info.bonding_curve)
|
||||
|
||||
|
||||
if token_info.pool_state: # For other platforms
|
||||
accounts.append(token_info.pool_state)
|
||||
|
||||
|
||||
return accounts
|
||||
|
||||
|
||||
@@ -85,6 +86,7 @@ class Trader(ABC):
|
||||
@dataclass
|
||||
class TokenInfo_Legacy:
|
||||
"""Legacy token information structure for backward compatibility."""
|
||||
|
||||
name: str
|
||||
symbol: str
|
||||
uri: str
|
||||
@@ -138,13 +140,13 @@ class TokenInfo_Legacy:
|
||||
|
||||
def upgrade_token_info(legacy_token_info: TokenInfo_Legacy) -> TokenInfo:
|
||||
"""Convert legacy TokenInfo to enhanced TokenInfo.
|
||||
|
||||
|
||||
This function allows existing code that creates legacy TokenInfo objects
|
||||
to be upgraded to the new enhanced format.
|
||||
|
||||
|
||||
Args:
|
||||
legacy_token_info: Legacy TokenInfo instance
|
||||
|
||||
|
||||
Returns:
|
||||
Enhanced TokenInfo with platform information
|
||||
"""
|
||||
@@ -164,29 +166,31 @@ def upgrade_token_info(legacy_token_info: TokenInfo_Legacy) -> TokenInfo:
|
||||
|
||||
def create_legacy_token_info(enhanced_token_info: TokenInfo) -> TokenInfo_Legacy:
|
||||
"""Convert enhanced TokenInfo back to legacy TokenInfo if needed.
|
||||
|
||||
|
||||
This function allows the enhanced TokenInfo to be used with existing
|
||||
code that expects the legacy format.
|
||||
|
||||
|
||||
Args:
|
||||
enhanced_token_info: Enhanced TokenInfo instance
|
||||
|
||||
|
||||
Returns:
|
||||
Legacy TokenInfo instance
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If enhanced TokenInfo doesn't have required pump.fun fields
|
||||
"""
|
||||
if enhanced_token_info.platform != Platform.PUMP_FUN:
|
||||
raise ValueError("Can only convert pump.fun tokens to legacy format")
|
||||
|
||||
if not all([
|
||||
enhanced_token_info.bonding_curve,
|
||||
enhanced_token_info.associated_bonding_curve,
|
||||
enhanced_token_info.creator_vault
|
||||
]):
|
||||
|
||||
if not all(
|
||||
[
|
||||
enhanced_token_info.bonding_curve,
|
||||
enhanced_token_info.associated_bonding_curve,
|
||||
enhanced_token_info.creator_vault,
|
||||
]
|
||||
):
|
||||
raise ValueError("Enhanced TokenInfo missing required pump.fun fields")
|
||||
|
||||
|
||||
return TokenInfo_Legacy(
|
||||
name=enhanced_token_info.name,
|
||||
symbol=enhanced_token_info.symbol,
|
||||
@@ -210,10 +214,10 @@ def create_pump_fun_token_info(
|
||||
user: Pubkey,
|
||||
creator: Pubkey | None = None,
|
||||
creator_vault: Pubkey | None = None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> TokenInfo:
|
||||
"""Convenience function to create pump.fun TokenInfo with proper platform setting.
|
||||
|
||||
|
||||
Args:
|
||||
name: Token name
|
||||
symbol: Token symbol
|
||||
@@ -225,7 +229,7 @@ def create_pump_fun_token_info(
|
||||
creator: Creator address (defaults to user if not provided)
|
||||
creator_vault: Creator vault address (will be derived if not provided)
|
||||
**kwargs: Additional fields for TokenInfo
|
||||
|
||||
|
||||
Returns:
|
||||
Enhanced TokenInfo configured for pump.fun
|
||||
"""
|
||||
@@ -234,7 +238,7 @@ def create_pump_fun_token_info(
|
||||
# We can't import PumpAddresses here, so this will need to be handled elsewhere
|
||||
# For now, leave it as None and let the platform implementation handle it
|
||||
pass
|
||||
|
||||
|
||||
return TokenInfo(
|
||||
name=name,
|
||||
symbol=symbol,
|
||||
@@ -246,7 +250,7 @@ def create_pump_fun_token_info(
|
||||
user=user,
|
||||
creator=creator or user,
|
||||
creator_vault=creator_vault,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
@@ -260,10 +264,10 @@ def create_lets_bonk_token_info(
|
||||
quote_vault: Pubkey,
|
||||
user: Pubkey,
|
||||
creator: Pubkey | None = None,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
) -> TokenInfo:
|
||||
"""Convenience function to create LetsBonk TokenInfo with proper platform setting.
|
||||
|
||||
|
||||
Args:
|
||||
name: Token name
|
||||
symbol: Token symbol
|
||||
@@ -275,7 +279,7 @@ def create_lets_bonk_token_info(
|
||||
user: User/trader address
|
||||
creator: Creator address (defaults to user if not provided)
|
||||
**kwargs: Additional fields for TokenInfo
|
||||
|
||||
|
||||
Returns:
|
||||
Enhanced TokenInfo configured for LetsBonk
|
||||
"""
|
||||
@@ -290,16 +294,16 @@ def create_lets_bonk_token_info(
|
||||
quote_vault=quote_vault,
|
||||
user=user,
|
||||
creator=creator or user,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def is_pump_fun_token(token_info: TokenInfo) -> bool:
|
||||
"""Check if a TokenInfo is for pump.fun platform.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information to check
|
||||
|
||||
|
||||
Returns:
|
||||
True if token is for pump.fun platform
|
||||
"""
|
||||
@@ -308,10 +312,10 @@ def is_pump_fun_token(token_info: TokenInfo) -> bool:
|
||||
|
||||
def is_lets_bonk_token(token_info: TokenInfo) -> bool:
|
||||
"""Check if a TokenInfo is for LetsBonk platform.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information to check
|
||||
|
||||
|
||||
Returns:
|
||||
True if token is for LetsBonk platform
|
||||
"""
|
||||
@@ -320,10 +324,10 @@ def is_lets_bonk_token(token_info: TokenInfo) -> bool:
|
||||
|
||||
def get_platform_specific_fields(token_info: TokenInfo) -> dict[str, Any]:
|
||||
"""Get platform-specific fields from TokenInfo.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary of platform-specific fields
|
||||
"""
|
||||
@@ -345,51 +349,57 @@ def get_platform_specific_fields(token_info: TokenInfo) -> dict[str, Any]:
|
||||
|
||||
def validate_token_info(token_info: TokenInfo) -> bool:
|
||||
"""Validate that TokenInfo has required fields for its platform.
|
||||
|
||||
|
||||
Args:
|
||||
token_info: Token information to validate
|
||||
|
||||
|
||||
Returns:
|
||||
True if TokenInfo is valid for its platform
|
||||
"""
|
||||
# Check common required fields
|
||||
if not all([
|
||||
token_info.name,
|
||||
token_info.symbol,
|
||||
token_info.mint,
|
||||
token_info.platform,
|
||||
]):
|
||||
if not all(
|
||||
[
|
||||
token_info.name,
|
||||
token_info.symbol,
|
||||
token_info.mint,
|
||||
token_info.platform,
|
||||
]
|
||||
):
|
||||
return False
|
||||
|
||||
|
||||
# Check platform-specific required fields
|
||||
if token_info.platform == Platform.PUMP_FUN:
|
||||
return all([
|
||||
token_info.bonding_curve,
|
||||
token_info.associated_bonding_curve,
|
||||
])
|
||||
return all(
|
||||
[
|
||||
token_info.bonding_curve,
|
||||
token_info.associated_bonding_curve,
|
||||
]
|
||||
)
|
||||
elif token_info.platform == Platform.LETS_BONK:
|
||||
return all([
|
||||
token_info.pool_state,
|
||||
token_info.base_vault,
|
||||
token_info.quote_vault,
|
||||
])
|
||||
|
||||
return all(
|
||||
[
|
||||
token_info.pool_state,
|
||||
token_info.base_vault,
|
||||
token_info.quote_vault,
|
||||
]
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# Backward compatibility exports
|
||||
__all__ = [
|
||||
'Platform', # Platform enum
|
||||
'TokenInfo', # Enhanced TokenInfo (main export)
|
||||
'TokenInfo_Legacy', # Legacy TokenInfo for compatibility
|
||||
'TradeResult', # Enhanced TradeResult
|
||||
'Trader', # Enhanced Trader base class
|
||||
'create_legacy_token_info',
|
||||
'create_lets_bonk_token_info',
|
||||
'create_pump_fun_token_info',
|
||||
'get_platform_specific_fields',
|
||||
'is_lets_bonk_token',
|
||||
'is_pump_fun_token',
|
||||
'upgrade_token_info',
|
||||
'validate_token_info',
|
||||
]
|
||||
"Platform", # Platform enum
|
||||
"TokenInfo", # Enhanced TokenInfo (main export)
|
||||
"TokenInfo_Legacy", # Legacy TokenInfo for compatibility
|
||||
"TradeResult", # Enhanced TradeResult
|
||||
"Trader", # Enhanced Trader base class
|
||||
"create_legacy_token_info",
|
||||
"create_lets_bonk_token_info",
|
||||
"create_pump_fun_token_info",
|
||||
"get_platform_specific_fields",
|
||||
"is_lets_bonk_token",
|
||||
"is_pump_fun_token",
|
||||
"upgrade_token_info",
|
||||
"validate_token_info",
|
||||
]
|
||||
|
||||
@@ -45,7 +45,9 @@ class PlatformAwareBuyer(Trader):
|
||||
"""Execute buy operation using platform-specific implementations."""
|
||||
try:
|
||||
# Get platform-specific implementations
|
||||
implementations = get_platform_implementations(token_info.platform, self.client)
|
||||
implementations = get_platform_implementations(
|
||||
token_info.platform, self.client
|
||||
)
|
||||
address_provider = implementations.address_provider
|
||||
instruction_builder = implementations.instruction_builder
|
||||
curve_manager = implementations.curve_manager
|
||||
@@ -60,10 +62,12 @@ class PlatformAwareBuyer(Trader):
|
||||
else:
|
||||
# Get pool address based on platform using platform-agnostic method
|
||||
pool_address = self._get_pool_address(token_info, address_provider)
|
||||
|
||||
|
||||
# Regular behavior with RPC call
|
||||
token_price_sol = await curve_manager.calculate_price(pool_address)
|
||||
token_amount = self.amount / token_price_sol if token_price_sol > 0 else 0
|
||||
token_amount = (
|
||||
self.amount / token_price_sol if token_price_sol > 0 else 0
|
||||
)
|
||||
|
||||
# Calculate minimum token amount with slippage
|
||||
minimum_token_amount = token_amount * (1 - self.slippage)
|
||||
@@ -78,7 +82,7 @@ class PlatformAwareBuyer(Trader):
|
||||
self.wallet.pubkey,
|
||||
max_amount_lamports, # amount_in (SOL)
|
||||
minimum_token_amount_raw, # minimum_amount_out (tokens)
|
||||
address_provider
|
||||
address_provider,
|
||||
)
|
||||
|
||||
# Get accounts for priority fee calculation
|
||||
@@ -125,21 +129,21 @@ class PlatformAwareBuyer(Trader):
|
||||
except Exception as e:
|
||||
logger.exception("Buy operation failed")
|
||||
return TradeResult(
|
||||
success=False,
|
||||
platform=token_info.platform,
|
||||
error_message=str(e)
|
||||
success=False, platform=token_info.platform, error_message=str(e)
|
||||
)
|
||||
|
||||
def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey:
|
||||
def _get_pool_address(
|
||||
self, token_info: TokenInfo, address_provider: AddressProvider
|
||||
) -> Pubkey:
|
||||
"""Get the pool/curve address for price calculations using platform-agnostic method."""
|
||||
# Try to get the address from token_info first, then derive if needed
|
||||
if token_info.platform == Platform.PUMP_FUN:
|
||||
if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve:
|
||||
if hasattr(token_info, "bonding_curve") and token_info.bonding_curve:
|
||||
return token_info.bonding_curve
|
||||
elif token_info.platform == Platform.LETS_BONK:
|
||||
if hasattr(token_info, 'pool_state') and token_info.pool_state:
|
||||
if hasattr(token_info, "pool_state") and token_info.pool_state:
|
||||
return token_info.pool_state
|
||||
|
||||
|
||||
# Fallback to deriving the address using platform provider
|
||||
return address_provider.derive_pool_address(token_info.mint)
|
||||
|
||||
@@ -166,7 +170,9 @@ class PlatformAwareSeller(Trader):
|
||||
"""Execute sell operation using platform-specific implementations."""
|
||||
try:
|
||||
# Get platform-specific implementations
|
||||
implementations = get_platform_implementations(token_info.platform, self.client)
|
||||
implementations = get_platform_implementations(
|
||||
token_info.platform, self.client
|
||||
)
|
||||
address_provider = implementations.address_provider
|
||||
instruction_builder = implementations.instruction_builder
|
||||
curve_manager = implementations.curve_manager
|
||||
@@ -175,8 +181,10 @@ class PlatformAwareSeller(Trader):
|
||||
user_token_account = address_provider.derive_user_token_account(
|
||||
self.wallet.pubkey, token_info.mint
|
||||
)
|
||||
|
||||
token_balance = await self.client.get_token_account_balance(user_token_account)
|
||||
|
||||
token_balance = await self.client.get_token_account_balance(
|
||||
user_token_account
|
||||
)
|
||||
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
|
||||
|
||||
logger.info(f"Token balance: {token_balance_decimal}")
|
||||
@@ -184,9 +192,9 @@ class PlatformAwareSeller(Trader):
|
||||
if token_balance == 0:
|
||||
logger.info("No tokens to sell.")
|
||||
return TradeResult(
|
||||
success=False,
|
||||
success=False,
|
||||
platform=token_info.platform,
|
||||
error_message="No tokens to sell"
|
||||
error_message="No tokens to sell",
|
||||
)
|
||||
|
||||
# Get pool address and current price using platform-agnostic method
|
||||
@@ -197,9 +205,13 @@ class PlatformAwareSeller(Trader):
|
||||
|
||||
# Calculate minimum SOL output with slippage
|
||||
expected_sol_output = float(token_balance_decimal) * float(token_price_sol)
|
||||
min_sol_output = int((expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL)
|
||||
min_sol_output = int(
|
||||
(expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL
|
||||
)
|
||||
|
||||
logger.info(f"Selling {token_balance_decimal} tokens on {token_info.platform.value}")
|
||||
logger.info(
|
||||
f"Selling {token_balance_decimal} tokens on {token_info.platform.value}"
|
||||
)
|
||||
logger.info(f"Expected SOL output: {expected_sol_output:.8f} SOL")
|
||||
logger.info(
|
||||
f"Minimum SOL output (with {self.slippage * 100}% slippage): {min_sol_output / LAMPORTS_PER_SOL:.8f} SOL"
|
||||
@@ -211,7 +223,7 @@ class PlatformAwareSeller(Trader):
|
||||
self.wallet.pubkey,
|
||||
token_balance, # amount_in (tokens)
|
||||
min_sol_output, # minimum_amount_out (SOL)
|
||||
address_provider
|
||||
address_provider,
|
||||
)
|
||||
|
||||
# Get accounts for priority fee calculation
|
||||
@@ -251,20 +263,20 @@ class PlatformAwareSeller(Trader):
|
||||
except Exception as e:
|
||||
logger.exception("Sell operation failed")
|
||||
return TradeResult(
|
||||
success=False,
|
||||
platform=token_info.platform,
|
||||
error_message=str(e)
|
||||
success=False, platform=token_info.platform, error_message=str(e)
|
||||
)
|
||||
|
||||
def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey:
|
||||
def _get_pool_address(
|
||||
self, token_info: TokenInfo, address_provider: AddressProvider
|
||||
) -> Pubkey:
|
||||
"""Get the pool/curve address for price calculations using platform-agnostic method."""
|
||||
# Try to get the address from token_info first, then derive if needed
|
||||
if token_info.platform == Platform.PUMP_FUN:
|
||||
if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve:
|
||||
if hasattr(token_info, "bonding_curve") and token_info.bonding_curve:
|
||||
return token_info.bonding_curve
|
||||
elif token_info.platform == Platform.LETS_BONK:
|
||||
if hasattr(token_info, 'pool_state') and token_info.pool_state:
|
||||
if hasattr(token_info, "pool_state") and token_info.pool_state:
|
||||
return token_info.pool_state
|
||||
|
||||
|
||||
# Fallback to deriving the address using platform provider
|
||||
return address_provider.derive_pool_address(token_info.mint)
|
||||
return address_provider.derive_pool_address(token_info.mint)
|
||||
|
||||
+132
-44
@@ -108,6 +108,7 @@ class UniversalTrader:
|
||||
# Validate platform support
|
||||
try:
|
||||
from platforms import platform_factory
|
||||
|
||||
if not platform_factory.registry.is_platform_supported(self.platform):
|
||||
raise ValueError(f"Platform {self.platform.value} is not supported")
|
||||
except Exception:
|
||||
@@ -130,7 +131,7 @@ class UniversalTrader:
|
||||
extreme_fast_token_amount,
|
||||
extreme_fast_mode,
|
||||
)
|
||||
|
||||
|
||||
self.seller = PlatformAwareSeller(
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
@@ -193,17 +194,27 @@ class UniversalTrader:
|
||||
async def start(self) -> None:
|
||||
"""Start the trading bot and listen for new tokens."""
|
||||
logger.info(f"Starting Universal Trader for {self.platform.value}")
|
||||
logger.info(f"Match filter: {self.match_string if self.match_string else 'None'}")
|
||||
logger.info(f"Creator filter: {self.bro_address if self.bro_address else 'None'}")
|
||||
logger.info(
|
||||
f"Match filter: {self.match_string if self.match_string else 'None'}"
|
||||
)
|
||||
logger.info(
|
||||
f"Creator filter: {self.bro_address if self.bro_address else 'None'}"
|
||||
)
|
||||
logger.info(f"Marry mode: {self.marry_mode}")
|
||||
logger.info(f"YOLO mode: {self.yolo_mode}")
|
||||
logger.info(f"Exit strategy: {self.exit_strategy}")
|
||||
|
||||
|
||||
if self.exit_strategy == "tp_sl":
|
||||
logger.info(f"Take profit: {self.take_profit_percentage * 100 if self.take_profit_percentage else 'None'}%")
|
||||
logger.info(f"Stop loss: {self.stop_loss_percentage * 100 if self.stop_loss_percentage else 'None'}%")
|
||||
logger.info(f"Max hold time: {self.max_hold_time if self.max_hold_time else 'None'} seconds")
|
||||
|
||||
logger.info(
|
||||
f"Take profit: {self.take_profit_percentage * 100 if self.take_profit_percentage else 'None'}%"
|
||||
)
|
||||
logger.info(
|
||||
f"Stop loss: {self.stop_loss_percentage * 100 if self.stop_loss_percentage else 'None'}%"
|
||||
)
|
||||
logger.info(
|
||||
f"Max hold time: {self.max_hold_time if self.max_hold_time else 'None'} seconds"
|
||||
)
|
||||
|
||||
logger.info(f"Max token age: {self.max_token_age} seconds")
|
||||
|
||||
try:
|
||||
@@ -216,16 +227,22 @@ class UniversalTrader:
|
||||
# Choose operating mode based on yolo_mode
|
||||
if not self.yolo_mode:
|
||||
# Single token mode: process one token and exit
|
||||
logger.info("Running in single token mode - will process one token and exit")
|
||||
logger.info(
|
||||
"Running in single token mode - will process one token and exit"
|
||||
)
|
||||
token_info = await self._wait_for_token()
|
||||
if token_info:
|
||||
await self._handle_token(token_info)
|
||||
logger.info("Finished processing single token. Exiting...")
|
||||
else:
|
||||
logger.info(f"No suitable token found within timeout period ({self.token_wait_timeout}s). Exiting...")
|
||||
logger.info(
|
||||
f"No suitable token found within timeout period ({self.token_wait_timeout}s). Exiting..."
|
||||
)
|
||||
else:
|
||||
# Continuous mode: process tokens until interrupted
|
||||
logger.info("Running in continuous mode - will process tokens until interrupted")
|
||||
logger.info(
|
||||
"Running in continuous mode - will process tokens until interrupted"
|
||||
)
|
||||
processor_task = asyncio.create_task(self._process_token_queue())
|
||||
|
||||
try:
|
||||
@@ -278,12 +295,16 @@ class UniversalTrader:
|
||||
|
||||
# Wait for a token with a timeout
|
||||
try:
|
||||
logger.info(f"Waiting for a suitable token (timeout: {self.token_wait_timeout}s)...")
|
||||
logger.info(
|
||||
f"Waiting for a suitable token (timeout: {self.token_wait_timeout}s)..."
|
||||
)
|
||||
await asyncio.wait_for(token_found.wait(), timeout=self.token_wait_timeout)
|
||||
logger.info(f"Found token: {found_token.symbol} ({found_token.mint})")
|
||||
return found_token
|
||||
except TimeoutError:
|
||||
logger.info(f"Timed out after waiting {self.token_wait_timeout}s for a token")
|
||||
logger.info(
|
||||
f"Timed out after waiting {self.token_wait_timeout}s for a token"
|
||||
)
|
||||
return None
|
||||
finally:
|
||||
listener_task.cancel()
|
||||
@@ -327,7 +348,9 @@ class UniversalTrader:
|
||||
self.token_timestamps[token_key] = monotonic()
|
||||
|
||||
await self.token_queue.put(token_info)
|
||||
logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint}) on {token_info.platform.value}")
|
||||
logger.info(
|
||||
f"Queued new token: {token_info.symbol} ({token_info.mint}) on {token_info.platform.value}"
|
||||
)
|
||||
|
||||
async def _process_token_queue(self) -> None:
|
||||
"""Continuously process tokens from the queue, only if they're fresh."""
|
||||
@@ -338,15 +361,21 @@ class UniversalTrader:
|
||||
|
||||
# Check if token is still "fresh"
|
||||
current_time = monotonic()
|
||||
token_age = current_time - self.token_timestamps.get(token_key, current_time)
|
||||
token_age = current_time - self.token_timestamps.get(
|
||||
token_key, current_time
|
||||
)
|
||||
|
||||
if token_age > self.max_token_age:
|
||||
logger.info(f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)")
|
||||
logger.info(
|
||||
f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)"
|
||||
)
|
||||
continue
|
||||
|
||||
self.processed_tokens.add(token_key)
|
||||
|
||||
logger.info(f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)")
|
||||
logger.info(
|
||||
f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)"
|
||||
)
|
||||
await self._handle_token(token_info)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
@@ -362,17 +391,23 @@ class UniversalTrader:
|
||||
try:
|
||||
# Validate that token is for our platform
|
||||
if token_info.platform != self.platform:
|
||||
logger.warning(f"Token platform mismatch: expected {self.platform.value}, got {token_info.platform.value}")
|
||||
logger.warning(
|
||||
f"Token platform mismatch: expected {self.platform.value}, got {token_info.platform.value}"
|
||||
)
|
||||
return
|
||||
|
||||
# Wait for pool/curve to stabilize (unless in extreme fast mode)
|
||||
if not self.extreme_fast_mode:
|
||||
await self._save_token_info(token_info)
|
||||
logger.info(f"Waiting for {self.wait_time_after_creation} seconds for the pool/curve to stabilize...")
|
||||
logger.info(
|
||||
f"Waiting for {self.wait_time_after_creation} seconds for the pool/curve to stabilize..."
|
||||
)
|
||||
await asyncio.sleep(self.wait_time_after_creation)
|
||||
|
||||
# Buy token
|
||||
logger.info(f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol} on {token_info.platform.value}...")
|
||||
logger.info(
|
||||
f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol} on {token_info.platform.value}..."
|
||||
)
|
||||
buy_result: TradeResult = await self.buyer.execute(token_info)
|
||||
|
||||
if buy_result.success:
|
||||
@@ -382,16 +417,28 @@ class UniversalTrader:
|
||||
|
||||
# Only wait for next token in yolo mode
|
||||
if self.yolo_mode:
|
||||
logger.info(f"YOLO mode enabled. Waiting {self.wait_time_before_new_token} seconds before looking for next token...")
|
||||
logger.info(
|
||||
f"YOLO mode enabled. Waiting {self.wait_time_before_new_token} seconds before looking for next token..."
|
||||
)
|
||||
await asyncio.sleep(self.wait_time_before_new_token)
|
||||
|
||||
except Exception:
|
||||
logger.exception(f"Error handling token {token_info.symbol}")
|
||||
|
||||
async def _handle_successful_buy(self, token_info: TokenInfo, buy_result: TradeResult) -> None:
|
||||
async def _handle_successful_buy(
|
||||
self, token_info: TokenInfo, buy_result: TradeResult
|
||||
) -> None:
|
||||
"""Handle successful token purchase."""
|
||||
logger.info(f"Successfully bought {token_info.symbol} on {token_info.platform.value}")
|
||||
self._log_trade("buy", token_info, buy_result.price, buy_result.amount, buy_result.tx_signature)
|
||||
logger.info(
|
||||
f"Successfully bought {token_info.symbol} on {token_info.platform.value}"
|
||||
)
|
||||
self._log_trade(
|
||||
"buy",
|
||||
token_info,
|
||||
buy_result.price,
|
||||
buy_result.amount,
|
||||
buy_result.tx_signature,
|
||||
)
|
||||
self.traded_mints.add(token_info.mint)
|
||||
|
||||
# Choose exit strategy
|
||||
@@ -405,7 +452,9 @@ class UniversalTrader:
|
||||
else:
|
||||
logger.info("Marry mode enabled. Skipping sell operation.")
|
||||
|
||||
async def _handle_failed_buy(self, token_info: TokenInfo, buy_result: TradeResult) -> None:
|
||||
async def _handle_failed_buy(
|
||||
self, token_info: TokenInfo, buy_result: TradeResult
|
||||
) -> None:
|
||||
"""Handle failed token purchase."""
|
||||
logger.error(f"Failed to buy {token_info.symbol}: {buy_result.error_message}")
|
||||
# Close ATA if enabled
|
||||
@@ -419,7 +468,9 @@ class UniversalTrader:
|
||||
self.cleanup_force_close_with_burn,
|
||||
)
|
||||
|
||||
async def _handle_tp_sl_exit(self, token_info: TokenInfo, buy_result: TradeResult) -> None:
|
||||
async def _handle_tp_sl_exit(
|
||||
self, token_info: TokenInfo, buy_result: TradeResult
|
||||
) -> None:
|
||||
"""Handle take profit/stop loss exit strategy."""
|
||||
# Create position
|
||||
position = Position.create_from_buy_result(
|
||||
@@ -451,7 +502,13 @@ class UniversalTrader:
|
||||
|
||||
if sell_result.success:
|
||||
logger.info(f"Successfully sold {token_info.symbol}")
|
||||
self._log_trade("sell", token_info, sell_result.price, sell_result.amount, sell_result.tx_signature)
|
||||
self._log_trade(
|
||||
"sell",
|
||||
token_info,
|
||||
sell_result.price,
|
||||
sell_result.amount,
|
||||
sell_result.tx_signature,
|
||||
)
|
||||
# Close ATA if enabled
|
||||
await handle_cleanup_after_sell(
|
||||
self.solana_client,
|
||||
@@ -463,11 +520,17 @@ class UniversalTrader:
|
||||
self.cleanup_force_close_with_burn,
|
||||
)
|
||||
else:
|
||||
logger.error(f"Failed to sell {token_info.symbol}: {sell_result.error_message}")
|
||||
logger.error(
|
||||
f"Failed to sell {token_info.symbol}: {sell_result.error_message}"
|
||||
)
|
||||
|
||||
async def _monitor_position_until_exit(self, token_info: TokenInfo, position: Position) -> None:
|
||||
async def _monitor_position_until_exit(
|
||||
self, token_info: TokenInfo, position: Position
|
||||
) -> None:
|
||||
"""Monitor a position until exit conditions are met."""
|
||||
logger.info(f"Starting position monitoring (check interval: {self.price_check_interval}s)")
|
||||
logger.info(
|
||||
f"Starting position monitoring (check interval: {self.price_check_interval}s)"
|
||||
)
|
||||
|
||||
# Get pool address for price monitoring using platform-agnostic method
|
||||
pool_address = self._get_pool_address(token_info)
|
||||
@@ -487,7 +550,9 @@ class UniversalTrader:
|
||||
|
||||
# Log PnL before exit
|
||||
pnl = position.get_pnl(current_price)
|
||||
logger.info(f"Position PnL: {pnl['price_change_pct']:.2f}% ({pnl['unrealized_pnl_sol']:.6f} SOL)")
|
||||
logger.info(
|
||||
f"Position PnL: {pnl['price_change_pct']:.2f}% ({pnl['unrealized_pnl_sol']:.6f} SOL)"
|
||||
)
|
||||
|
||||
# Execute sell
|
||||
sell_result = await self.seller.execute(token_info)
|
||||
@@ -496,12 +561,22 @@ class UniversalTrader:
|
||||
# Close position with actual exit price
|
||||
position.close_position(sell_result.price, exit_reason)
|
||||
|
||||
logger.info(f"Successfully exited position: {exit_reason.value}")
|
||||
self._log_trade("sell", token_info, sell_result.price, sell_result.amount, sell_result.tx_signature)
|
||||
logger.info(
|
||||
f"Successfully exited position: {exit_reason.value}"
|
||||
)
|
||||
self._log_trade(
|
||||
"sell",
|
||||
token_info,
|
||||
sell_result.price,
|
||||
sell_result.amount,
|
||||
sell_result.tx_signature,
|
||||
)
|
||||
|
||||
# Log final PnL
|
||||
final_pnl = position.get_pnl()
|
||||
logger.info(f"Final PnL: {final_pnl['price_change_pct']:.2f}% ({final_pnl['unrealized_pnl_sol']:.6f} SOL)")
|
||||
logger.info(
|
||||
f"Final PnL: {final_pnl['price_change_pct']:.2f}% ({final_pnl['unrealized_pnl_sol']:.6f} SOL)"
|
||||
)
|
||||
|
||||
# Close ATA if enabled
|
||||
await handle_cleanup_after_sell(
|
||||
@@ -514,30 +589,36 @@ class UniversalTrader:
|
||||
self.cleanup_force_close_with_burn,
|
||||
)
|
||||
else:
|
||||
logger.error(f"Failed to exit position: {sell_result.error_message}")
|
||||
logger.error(
|
||||
f"Failed to exit position: {sell_result.error_message}"
|
||||
)
|
||||
# Keep monitoring in case sell can be retried
|
||||
|
||||
break
|
||||
else:
|
||||
# Log current status
|
||||
pnl = position.get_pnl(current_price)
|
||||
logger.debug(f"Position status: {current_price:.8f} SOL ({pnl['price_change_pct']:+.2f}%)")
|
||||
logger.debug(
|
||||
f"Position status: {current_price:.8f} SOL ({pnl['price_change_pct']:+.2f}%)"
|
||||
)
|
||||
|
||||
# Wait before next price check
|
||||
await asyncio.sleep(self.price_check_interval)
|
||||
|
||||
except Exception:
|
||||
logger.exception("Error monitoring position")
|
||||
await asyncio.sleep(self.price_check_interval) # Continue monitoring despite errors
|
||||
await asyncio.sleep(
|
||||
self.price_check_interval
|
||||
) # Continue monitoring despite errors
|
||||
|
||||
def _get_pool_address(self, token_info: TokenInfo) -> Pubkey:
|
||||
"""Get the pool/curve address for price monitoring using platform-agnostic method."""
|
||||
address_provider = self.platform_implementations.address_provider
|
||||
|
||||
|
||||
# Use platform-specific logic to get the appropriate address
|
||||
if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve:
|
||||
if hasattr(token_info, "bonding_curve") and token_info.bonding_curve:
|
||||
return token_info.bonding_curve
|
||||
elif hasattr(token_info, 'pool_state') and token_info.pool_state:
|
||||
elif hasattr(token_info, "pool_state") and token_info.pool_state:
|
||||
return token_info.pool_state
|
||||
else:
|
||||
# Fallback to deriving the address using platform provider
|
||||
@@ -561,7 +642,7 @@ class UniversalTrader:
|
||||
"creator": str(token_info.creator) if token_info.creator else None,
|
||||
"creation_timestamp": token_info.creation_timestamp,
|
||||
}
|
||||
|
||||
|
||||
# Add platform-specific fields only if they exist
|
||||
platform_fields = {
|
||||
"bonding_curve": token_info.bonding_curve,
|
||||
@@ -571,7 +652,7 @@ class UniversalTrader:
|
||||
"base_vault": token_info.base_vault,
|
||||
"quote_vault": token_info.quote_vault,
|
||||
}
|
||||
|
||||
|
||||
for field_name, field_value in platform_fields.items():
|
||||
if field_value is not None:
|
||||
token_dict[field_name] = str(field_value)
|
||||
@@ -582,7 +663,14 @@ class UniversalTrader:
|
||||
except OSError:
|
||||
logger.exception("Failed to save token information")
|
||||
|
||||
def _log_trade(self, action: str, token_info: TokenInfo, price: float, amount: float, tx_hash: str | None) -> None:
|
||||
def _log_trade(
|
||||
self,
|
||||
action: str,
|
||||
token_info: TokenInfo,
|
||||
price: float,
|
||||
amount: float,
|
||||
tx_hash: str | None,
|
||||
) -> None:
|
||||
"""Log trade information."""
|
||||
try:
|
||||
trades_dir = Path("trades")
|
||||
@@ -607,4 +695,4 @@ class UniversalTrader:
|
||||
|
||||
|
||||
# Backward compatibility alias
|
||||
PumpTrader = UniversalTrader # Legacy name for backward compatibility
|
||||
PumpTrader = UniversalTrader # Legacy name for backward compatibility
|
||||
|
||||
+81
-68
@@ -17,85 +17,91 @@ logger = get_logger(__name__)
|
||||
|
||||
class IDLManager:
|
||||
"""Centralized manager for IDL parsers across all platforms."""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the IDL manager."""
|
||||
self._parsers: dict[Platform, IDLParser] = {}
|
||||
self._idl_paths: dict[Platform, str] = {}
|
||||
self._setup_platform_idl_paths()
|
||||
|
||||
|
||||
def _setup_platform_idl_paths(self) -> None:
|
||||
"""Setup IDL file paths for each platform."""
|
||||
# Get the project root directory (3 levels up from this file)
|
||||
current_file = Path(__file__)
|
||||
project_root = current_file.parent.parent.parent
|
||||
|
||||
|
||||
# Define IDL paths for each platform
|
||||
self._idl_paths = {
|
||||
Platform.LETS_BONK: project_root / "idl" / "raydium_launchlab_idl.json",
|
||||
Platform.PUMP_FUN: project_root / "idl" / "pump_fun_idl.json",
|
||||
}
|
||||
|
||||
|
||||
def get_parser(self, platform: Platform, verbose: bool = False) -> IDLParser:
|
||||
"""Get or create an IDL parser for the specified platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get parser for
|
||||
verbose: Whether to enable verbose logging in the parser
|
||||
|
||||
|
||||
Returns:
|
||||
IDLParser instance for the platform
|
||||
|
||||
|
||||
Raises:
|
||||
ValueError: If platform is not supported or IDL file not found
|
||||
"""
|
||||
# Return cached parser if available
|
||||
if platform in self._parsers:
|
||||
return self._parsers[platform]
|
||||
|
||||
|
||||
# Check if platform has IDL support
|
||||
if platform not in self._idl_paths:
|
||||
raise ValueError(f"Platform {platform.value} does not have IDL support configured")
|
||||
|
||||
raise ValueError(
|
||||
f"Platform {platform.value} does not have IDL support configured"
|
||||
)
|
||||
|
||||
idl_path = self._idl_paths[platform]
|
||||
|
||||
|
||||
# Verify IDL file exists
|
||||
if not idl_path.exists():
|
||||
raise FileNotFoundError(f"IDL file not found for {platform.value} at {idl_path}")
|
||||
|
||||
raise FileNotFoundError(
|
||||
f"IDL file not found for {platform.value} at {idl_path}"
|
||||
)
|
||||
|
||||
# Load and cache the parser
|
||||
logger.info(f"Loading IDL parser for {platform.value} from {idl_path}")
|
||||
parser = IDLParser(idl_path, verbose=verbose)
|
||||
self._parsers[platform] = parser
|
||||
|
||||
|
||||
instruction_count = len(parser.get_instruction_names())
|
||||
event_count = len(parser.get_event_names())
|
||||
logger.info(f"IDL parser loaded for {platform.value} with {instruction_count} instructions and {event_count} events")
|
||||
|
||||
logger.info(
|
||||
f"IDL parser loaded for {platform.value} with {instruction_count} instructions and {event_count} events"
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def has_idl_support(self, platform: Platform) -> bool:
|
||||
"""Check if a platform has IDL support configured.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to check
|
||||
|
||||
|
||||
Returns:
|
||||
True if platform has IDL support
|
||||
"""
|
||||
return platform in self._idl_paths
|
||||
|
||||
|
||||
def get_supported_platforms(self) -> list[Platform]:
|
||||
"""Get list of platforms with IDL support.
|
||||
|
||||
|
||||
Returns:
|
||||
List of platforms that have IDL files configured
|
||||
"""
|
||||
return list(self._idl_paths.keys())
|
||||
|
||||
|
||||
def clear_cache(self, platform: Platform | None = None) -> None:
|
||||
"""Clear cached parsers.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Specific platform to clear, or None to clear all
|
||||
"""
|
||||
@@ -105,12 +111,12 @@ class IDLManager:
|
||||
elif platform in self._parsers:
|
||||
logger.info(f"Clearing cached IDL parser for {platform.value}")
|
||||
del self._parsers[platform]
|
||||
|
||||
|
||||
def preload_parser(self, platform: Platform, verbose: bool = False) -> None:
|
||||
"""Preload IDL parser for a platform.
|
||||
|
||||
|
||||
This can be useful for warming up the parser during initialization.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to preload parser for
|
||||
verbose: Whether to enable verbose logging in the parser
|
||||
@@ -120,101 +126,105 @@ class IDLManager:
|
||||
self.get_parser(platform, verbose)
|
||||
else:
|
||||
logger.debug(f"IDL parser for {platform.value} already loaded")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Instruction-related convenience methods
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_instruction_discriminators(self, platform: Platform) -> dict[str, bytes]:
|
||||
"""Get instruction discriminators for a platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get discriminators for
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary mapping instruction names to discriminator bytes
|
||||
"""
|
||||
parser = self.get_parser(platform)
|
||||
return parser.get_instruction_discriminators()
|
||||
|
||||
|
||||
def get_instruction_names(self, platform: Platform) -> list[str]:
|
||||
"""Get available instruction names for a platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get instruction names for
|
||||
|
||||
|
||||
Returns:
|
||||
List of instruction names
|
||||
"""
|
||||
parser = self.get_parser(platform)
|
||||
return parser.get_instruction_names()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Event-related convenience methods
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_event_discriminators(self, platform: Platform) -> dict[str, bytes]:
|
||||
"""Get event discriminators for a platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get event discriminators for
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary mapping event names to discriminator bytes
|
||||
"""
|
||||
parser = self.get_parser(platform)
|
||||
return parser.get_event_discriminators()
|
||||
|
||||
|
||||
def get_event_names(self, platform: Platform) -> list[str]:
|
||||
"""Get available event names for a platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get event names for
|
||||
|
||||
|
||||
Returns:
|
||||
List of event names
|
||||
"""
|
||||
parser = self.get_parser(platform)
|
||||
return parser.get_event_names()
|
||||
|
||||
def decode_event_from_logs(self, platform: Platform, logs: list[str], event_name: str | None = None) -> dict | None:
|
||||
|
||||
def decode_event_from_logs(
|
||||
self, platform: Platform, logs: list[str], event_name: str | None = None
|
||||
) -> dict | None:
|
||||
"""Decode event data from transaction logs for a platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to use for decoding
|
||||
logs: List of log strings from transaction
|
||||
event_name: Optional specific event name to look for
|
||||
|
||||
|
||||
Returns:
|
||||
Decoded event data if found, None otherwise
|
||||
"""
|
||||
parser = self.get_parser(platform)
|
||||
return parser.find_event_in_logs(logs, event_name)
|
||||
|
||||
def decode_event_data(self, platform: Platform, event_data: bytes, event_name: str | None = None) -> dict | None:
|
||||
|
||||
def decode_event_data(
|
||||
self, platform: Platform, event_data: bytes, event_name: str | None = None
|
||||
) -> dict | None:
|
||||
"""Decode raw event data for a platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to use for decoding
|
||||
event_data: Raw event data bytes
|
||||
event_name: Optional event name to decode as
|
||||
|
||||
|
||||
Returns:
|
||||
Decoded event data if successful, None otherwise
|
||||
"""
|
||||
parser = self.get_parser(platform)
|
||||
return parser.decode_event_data(event_data, event_name)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Platform information methods
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_platform_capabilities(self, platform: Platform) -> dict[str, Any]:
|
||||
"""Get comprehensive capability information for a platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get capabilities for
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary with platform capabilities
|
||||
"""
|
||||
@@ -227,12 +237,12 @@ class IDLManager:
|
||||
"instruction_count": 0,
|
||||
"event_count": 0,
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
parser = self.get_parser(platform)
|
||||
instruction_names = parser.get_instruction_names()
|
||||
event_names = parser.get_event_names()
|
||||
|
||||
|
||||
return {
|
||||
"platform": platform.value,
|
||||
"has_idl_support": True,
|
||||
@@ -260,7 +270,7 @@ _idl_manager: IDLManager | None = None
|
||||
|
||||
def get_idl_manager() -> IDLManager:
|
||||
"""Get the global IDL manager instance.
|
||||
|
||||
|
||||
Returns:
|
||||
Global IDLManager instance
|
||||
"""
|
||||
@@ -272,11 +282,11 @@ def get_idl_manager() -> IDLManager:
|
||||
|
||||
def get_idl_parser(platform: Platform, verbose: bool = False) -> IDLParser:
|
||||
"""Convenience function to get an IDL parser for a platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get parser for
|
||||
verbose: Whether to enable verbose logging in the parser
|
||||
|
||||
|
||||
Returns:
|
||||
IDLParser instance for the platform
|
||||
"""
|
||||
@@ -285,10 +295,10 @@ def get_idl_parser(platform: Platform, verbose: bool = False) -> IDLParser:
|
||||
|
||||
def has_idl_support(platform: Platform) -> bool:
|
||||
"""Check if a platform has IDL support.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to check
|
||||
|
||||
|
||||
Returns:
|
||||
True if platform has IDL support
|
||||
"""
|
||||
@@ -297,7 +307,7 @@ def has_idl_support(platform: Platform) -> bool:
|
||||
|
||||
def preload_platform_idl(platform: Platform, verbose: bool = False) -> None:
|
||||
"""Preload IDL parser for a platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to preload parser for
|
||||
verbose: Whether to enable verbose logging
|
||||
@@ -309,12 +319,13 @@ def preload_platform_idl(platform: Platform, verbose: bool = False) -> None:
|
||||
# Convenience functions for event handling
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_event_discriminators(platform: Platform) -> dict[str, bytes]:
|
||||
"""Convenience function to get event discriminators for a platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get event discriminators for
|
||||
|
||||
|
||||
Returns:
|
||||
Dictionary mapping event names to discriminator bytes
|
||||
"""
|
||||
@@ -323,25 +334,27 @@ def get_event_discriminators(platform: Platform) -> dict[str, bytes]:
|
||||
|
||||
def get_event_names(platform: Platform) -> list[str]:
|
||||
"""Convenience function to get event names for a platform.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to get event names for
|
||||
|
||||
|
||||
Returns:
|
||||
List of event names
|
||||
"""
|
||||
return get_idl_manager().get_event_names(platform)
|
||||
|
||||
|
||||
def decode_event_from_logs(platform: Platform, logs: list[str], event_name: str | None = None) -> dict | None:
|
||||
def decode_event_from_logs(
|
||||
platform: Platform, logs: list[str], event_name: str | None = None
|
||||
) -> dict | None:
|
||||
"""Convenience function to decode events from logs.
|
||||
|
||||
|
||||
Args:
|
||||
platform: Platform to use for decoding
|
||||
logs: List of log strings from transaction
|
||||
event_name: Optional specific event name to look for
|
||||
|
||||
|
||||
Returns:
|
||||
Decoded event data if found, None otherwise
|
||||
"""
|
||||
return get_idl_manager().decode_event_from_logs(platform, logs, event_name)
|
||||
return get_idl_manager().decode_event_from_logs(platform, logs, event_name)
|
||||
|
||||
+177
-133
@@ -24,17 +24,20 @@ class IDLParser:
|
||||
# to its struct format character and size in bytes.
|
||||
_PRIMITIVE_TYPE_INFO = {
|
||||
# type_name: (format_char, size_in_bytes)
|
||||
'u8': ('<B', 1),
|
||||
'u16': ('<H', 2),
|
||||
'u32': ('<I', 4),
|
||||
'u64': ('<Q', 8),
|
||||
'i8': ('<b', 1),
|
||||
'i16': ('<h', 2),
|
||||
'i32': ('<i', 4),
|
||||
'i64': ('<q', 8),
|
||||
'bool': ('<?', 1),
|
||||
'pubkey': (None, PUBLIC_KEY_SIZE),
|
||||
'string': (None, STRING_LENGTH_PREFIX_SIZE), # Min size is for the length prefix
|
||||
"u8": ("<B", 1),
|
||||
"u16": ("<H", 2),
|
||||
"u32": ("<I", 4),
|
||||
"u64": ("<Q", 8),
|
||||
"i8": ("<b", 1),
|
||||
"i16": ("<h", 2),
|
||||
"i32": ("<i", 4),
|
||||
"i64": ("<q", 8),
|
||||
"bool": ("<?", 1),
|
||||
"pubkey": (None, PUBLIC_KEY_SIZE),
|
||||
"string": (
|
||||
None,
|
||||
STRING_LENGTH_PREFIX_SIZE,
|
||||
), # Min size is for the length prefix
|
||||
}
|
||||
|
||||
def __init__(self, idl_path: str, verbose: bool = False):
|
||||
@@ -63,13 +66,15 @@ class IDLParser:
|
||||
|
||||
def get_instruction_discriminators(self) -> dict[str, bytes]:
|
||||
"""Get a mapping of instruction names to their discriminators."""
|
||||
return {instr['name']: disc for disc, instr in self.instructions.items()}
|
||||
return {instr["name"]: disc for disc, instr in self.instructions.items()}
|
||||
|
||||
def get_instruction_names(self) -> list[str]:
|
||||
"""Get a list of all available instruction names."""
|
||||
return [instr['name'] for instr in self.instructions.values()]
|
||||
return [instr["name"] for instr in self.instructions.values()]
|
||||
|
||||
def validate_instruction_data_length(self, ix_data: bytes, discriminator: bytes) -> bool:
|
||||
def validate_instruction_data_length(
|
||||
self, ix_data: bytes, discriminator: bytes
|
||||
) -> bool:
|
||||
"""Validate that instruction data meets minimum length requirements."""
|
||||
if discriminator not in self.instruction_min_sizes:
|
||||
return True # Allow if we don't know the expected size
|
||||
@@ -78,7 +83,7 @@ class IDLParser:
|
||||
actual_size = len(ix_data)
|
||||
|
||||
if actual_size < expected_min_size:
|
||||
instruction_name = self.instructions[discriminator]['name']
|
||||
instruction_name = self.instructions[discriminator]["name"]
|
||||
if self.verbose:
|
||||
print(
|
||||
f"⚠️ Instruction data for '{instruction_name}' is shorter than the expected minimum "
|
||||
@@ -88,7 +93,9 @@ class IDLParser:
|
||||
|
||||
return True
|
||||
|
||||
def decode_instruction(self, ix_data: bytes, keys: list[bytes], accounts: list[int]) -> dict[str, Any] | None:
|
||||
def decode_instruction(
|
||||
self, ix_data: bytes, keys: list[bytes], accounts: list[int]
|
||||
) -> dict[str, Any] | None:
|
||||
"""Decode instruction data using IDL definitions."""
|
||||
if len(ix_data) < DISCRIMINATOR_SIZE:
|
||||
return None
|
||||
@@ -102,14 +109,16 @@ class IDLParser:
|
||||
|
||||
instruction = self.instructions[discriminator]
|
||||
data_args = ix_data[DISCRIMINATOR_SIZE:]
|
||||
|
||||
|
||||
# Decode instruction arguments
|
||||
args = {}
|
||||
decode_offset = 0
|
||||
for arg in instruction.get('args', []):
|
||||
for arg in instruction.get("args", []):
|
||||
try:
|
||||
value, decode_offset = self._decode_type(data_args, decode_offset, arg['type'])
|
||||
args[arg['name']] = value
|
||||
value, decode_offset = self._decode_type(
|
||||
data_args, decode_offset, arg["type"]
|
||||
)
|
||||
args[arg["name"]] = value
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"❌ Decode error in argument '{arg['name']}': {e}")
|
||||
@@ -120,19 +129,19 @@ class IDLParser:
|
||||
if index < len(accounts):
|
||||
account_index = accounts[index]
|
||||
if account_index < len(keys):
|
||||
return base58.b58encode(keys[account_index]).decode('utf-8')
|
||||
return None # Return None for invalid indices
|
||||
return base58.b58encode(keys[account_index]).decode("utf-8")
|
||||
return None # Return None for invalid indices
|
||||
|
||||
# Build account info based on instruction definition
|
||||
account_info = {}
|
||||
instruction_accounts = instruction.get('accounts', [])
|
||||
instruction_accounts = instruction.get("accounts", [])
|
||||
for i, account_def in enumerate(instruction_accounts):
|
||||
account_info[account_def['name']] = get_account_key(i)
|
||||
account_info[account_def["name"]] = get_account_key(i)
|
||||
|
||||
return {
|
||||
'instruction_name': instruction['name'],
|
||||
'args': args,
|
||||
'accounts': account_info
|
||||
"instruction_name": instruction["name"],
|
||||
"args": args,
|
||||
"accounts": account_info,
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
@@ -141,20 +150,22 @@ class IDLParser:
|
||||
|
||||
def get_event_discriminators(self) -> dict[str, bytes]:
|
||||
"""Get a mapping of event names to their discriminators."""
|
||||
return {event['name']: disc for disc, event in self.events.items()}
|
||||
return {event["name"]: disc for disc, event in self.events.items()}
|
||||
|
||||
def get_event_names(self) -> list[str]:
|
||||
"""Get a list of all available event names."""
|
||||
return [event['name'] for event in self.events.values()]
|
||||
return [event["name"] for event in self.events.values()]
|
||||
|
||||
def decode_event_data(self, event_data: bytes, event_name: str | None = None) -> dict[str, Any] | None:
|
||||
def decode_event_data(
|
||||
self, event_data: bytes, event_name: str | None = None
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Decode event data using IDL event definitions.
|
||||
|
||||
|
||||
Args:
|
||||
event_data: Raw event data bytes (typically from base64 decoded log data)
|
||||
event_name: Optional event name to decode as. If None, will try to match discriminator.
|
||||
|
||||
|
||||
Returns:
|
||||
Decoded event data as a dictionary, or None if decoding fails.
|
||||
"""
|
||||
@@ -162,89 +173,94 @@ class IDLParser:
|
||||
return None
|
||||
|
||||
discriminator = event_data[:DISCRIMINATOR_SIZE]
|
||||
|
||||
|
||||
# Find event definition by discriminator in events section
|
||||
if discriminator not in self.events:
|
||||
if self.verbose:
|
||||
print(f"Unknown event discriminator: {discriminator.hex()}")
|
||||
return None
|
||||
|
||||
|
||||
event_def = self.events[discriminator]
|
||||
|
||||
|
||||
# If event_name provided, validate it matches
|
||||
if event_name and event_def['name'] != event_name:
|
||||
if event_name and event_def["name"] != event_name:
|
||||
if self.verbose:
|
||||
print(f"Event name mismatch: expected {event_name}, got {event_def['name']}")
|
||||
print(
|
||||
f"Event name mismatch: expected {event_name}, got {event_def['name']}"
|
||||
)
|
||||
return None
|
||||
|
||||
# Get the actual structure definition from types section
|
||||
event_name_actual = event_def['name']
|
||||
event_name_actual = event_def["name"]
|
||||
if event_name_actual not in self.types:
|
||||
if self.verbose:
|
||||
print(f"Event type {event_name_actual} not found in types section")
|
||||
return None
|
||||
|
||||
|
||||
type_def = self.types[event_name_actual]
|
||||
event_type = type_def.get('type', {})
|
||||
|
||||
event_type = type_def.get("type", {})
|
||||
|
||||
# Decode event fields
|
||||
try:
|
||||
event_fields = {}
|
||||
data_part = event_data[DISCRIMINATOR_SIZE:]
|
||||
decode_offset = 0
|
||||
|
||||
if event_type.get('kind') != 'struct':
|
||||
|
||||
if event_type.get("kind") != "struct":
|
||||
if self.verbose:
|
||||
print(f"Event {event_name_actual} is not a struct type: {event_type.get('kind', 'NO KIND')}")
|
||||
print(
|
||||
f"Event {event_name_actual} is not a struct type: {event_type.get('kind', 'NO KIND')}"
|
||||
)
|
||||
print(f"Available keys in type_def: {list(type_def.keys())}")
|
||||
print(f"Event type structure: {event_type}")
|
||||
return None
|
||||
|
||||
|
||||
# Decode each field in the struct
|
||||
fields = event_type.get('fields', [])
|
||||
fields = event_type.get("fields", [])
|
||||
if self.verbose:
|
||||
print(f"Decoding {len(fields)} fields for event {event_name_actual}")
|
||||
|
||||
|
||||
for field in fields:
|
||||
if self.verbose:
|
||||
print(f"Decoding field: {field['name']} ({field['type']})")
|
||||
|
||||
|
||||
try:
|
||||
value, decode_offset = self._decode_type(data_part, decode_offset, field['type'])
|
||||
event_fields[field['name']] = value
|
||||
|
||||
value, decode_offset = self._decode_type(
|
||||
data_part, decode_offset, field["type"]
|
||||
)
|
||||
event_fields[field["name"]] = value
|
||||
|
||||
if self.verbose:
|
||||
if field['type'] == 'string':
|
||||
if field["type"] == "string":
|
||||
print(f" -> '{value}'")
|
||||
elif field['type'] == 'pubkey':
|
||||
elif field["type"] == "pubkey":
|
||||
print(f" -> {value}")
|
||||
else:
|
||||
print(f" -> {value}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"Error decoding field {field['name']}: {e}")
|
||||
# Don't return None here, continue with other fields
|
||||
continue
|
||||
|
||||
return {
|
||||
'event_name': event_name_actual,
|
||||
'fields': event_fields
|
||||
}
|
||||
return {"event_name": event_name_actual, "fields": event_fields}
|
||||
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"❌ Error decoding event {event_name_actual}: {e}")
|
||||
return None
|
||||
|
||||
def find_event_in_logs(self, logs: list[str], target_event_name: str | None = None) -> dict[str, Any] | None:
|
||||
def find_event_in_logs(
|
||||
self, logs: list[str], target_event_name: str | None = None
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Find and decode event data from transaction logs.
|
||||
|
||||
|
||||
Args:
|
||||
logs: List of log strings from a transaction
|
||||
target_event_name: Optional specific event name to look for
|
||||
|
||||
|
||||
Returns:
|
||||
Decoded event data if found, None otherwise
|
||||
"""
|
||||
@@ -254,34 +270,39 @@ class IDLParser:
|
||||
# Extract base64 encoded data
|
||||
encoded_data = log.split("Program data: ")[1].strip()
|
||||
decoded_data = base64.b64decode(encoded_data)
|
||||
|
||||
|
||||
# Try to decode as event
|
||||
event_data = self.decode_event_data(decoded_data, target_event_name)
|
||||
if event_data:
|
||||
return event_data
|
||||
|
||||
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
print(f"Failed to decode log data: {e}")
|
||||
continue
|
||||
|
||||
|
||||
return None
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Public Methods (External API) - Account Data
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def decode_account_data(self, account_data: bytes, account_type_name: str, skip_discriminator: bool = True) -> dict[str, Any] | None:
|
||||
def decode_account_data(
|
||||
self,
|
||||
account_data: bytes,
|
||||
account_type_name: str,
|
||||
skip_discriminator: bool = True,
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Decode account data using a specific account type from the IDL.
|
||||
|
||||
|
||||
Args:
|
||||
account_data: Raw account data bytes.
|
||||
account_type_name: Name of the account type in the IDL (e.g., "MyAccount").
|
||||
skip_discriminator: Whether to skip the first 8 bytes, which Anchor uses as a
|
||||
type discriminator for account data. Set to False if your
|
||||
data does not have this prefix.
|
||||
|
||||
|
||||
Returns:
|
||||
Decoded account data as a dictionary, or None if decoding fails.
|
||||
"""
|
||||
@@ -295,7 +316,9 @@ class IDLParser:
|
||||
if skip_discriminator:
|
||||
if len(account_data) < DISCRIMINATOR_SIZE:
|
||||
if self.verbose:
|
||||
print(f"Account data too short to contain a discriminator: {len(account_data)} bytes")
|
||||
print(
|
||||
f"Account data too short to contain a discriminator: {len(account_data)} bytes"
|
||||
)
|
||||
return None
|
||||
data = account_data[DISCRIMINATOR_SIZE:]
|
||||
|
||||
@@ -313,34 +336,36 @@ class IDLParser:
|
||||
|
||||
def _build_instruction_map(self):
|
||||
"""Build a map of discriminators to instruction definitions."""
|
||||
for instruction in self.idl.get('instructions', []):
|
||||
for instruction in self.idl.get("instructions", []):
|
||||
# The discriminator from the JSON IDL is a list of u8 integers.
|
||||
discriminator = bytes(instruction['discriminator'])
|
||||
discriminator = bytes(instruction["discriminator"])
|
||||
self.instructions[discriminator] = instruction
|
||||
|
||||
def _build_event_map(self):
|
||||
"""Build a map of discriminators to event definitions."""
|
||||
for event in self.idl.get('events', []):
|
||||
for event in self.idl.get("events", []):
|
||||
# The discriminator from the JSON IDL is a list of u8 integers.
|
||||
discriminator = bytes(event['discriminator'])
|
||||
discriminator = bytes(event["discriminator"])
|
||||
self.events[discriminator] = event
|
||||
if self.verbose:
|
||||
print(f"📅 Loaded event: {event['name']} with discriminator {discriminator.hex()}")
|
||||
print(
|
||||
f"📅 Loaded event: {event['name']} with discriminator {discriminator.hex()}"
|
||||
)
|
||||
|
||||
def _build_type_map(self):
|
||||
"""Build a map of type names to their definitions."""
|
||||
for type_def in self.idl.get('types', []):
|
||||
self.types[type_def['name']] = type_def
|
||||
for type_def in self.idl.get("types", []):
|
||||
self.types[type_def["name"]] = type_def
|
||||
|
||||
def _calculate_instruction_sizes(self):
|
||||
"""Calculate minimum data sizes for each instruction."""
|
||||
for discriminator, instruction in self.instructions.items():
|
||||
try:
|
||||
min_size = DISCRIMINATOR_SIZE
|
||||
for arg in instruction.get('args', []):
|
||||
min_size += self._calculate_type_min_size(arg['type'])
|
||||
for arg in instruction.get("args", []):
|
||||
min_size += self._calculate_type_min_size(arg["type"])
|
||||
self.instruction_min_sizes[discriminator] = min_size
|
||||
if self.verbose and instruction['name'] == 'initialize':
|
||||
if self.verbose and instruction["name"] == "initialize":
|
||||
print(f"📏 Initialize instruction min size: {min_size} bytes")
|
||||
except Exception as e:
|
||||
if self.verbose:
|
||||
@@ -351,17 +376,19 @@ class IDLParser:
|
||||
"""Calculate minimum size in bytes for a type definition."""
|
||||
if isinstance(type_def, str):
|
||||
return self._get_primitive_size(type_def)
|
||||
|
||||
|
||||
if isinstance(type_def, dict):
|
||||
if 'defined' in type_def:
|
||||
if "defined" in type_def:
|
||||
type_name = self._get_defined_type_name(type_def)
|
||||
return self._calculate_defined_type_min_size(type_name)
|
||||
if 'array' in type_def:
|
||||
element_type, array_length = type_def['array']
|
||||
if "array" in type_def:
|
||||
element_type, array_length = type_def["array"]
|
||||
element_size = self._calculate_type_min_size(element_type)
|
||||
return element_size * array_length
|
||||
|
||||
raise ValueError(f"Invalid or unknown type definition for size calculation: {type_def}")
|
||||
|
||||
raise ValueError(
|
||||
f"Invalid or unknown type definition for size calculation: {type_def}"
|
||||
)
|
||||
|
||||
def _get_primitive_size(self, type_name: str) -> int:
|
||||
"""Get size in bytes for primitive types from the central map."""
|
||||
@@ -370,51 +397,62 @@ class IDLParser:
|
||||
|
||||
def _get_defined_type_name(self, type_def: dict[str, Any]) -> str:
|
||||
"""Extracts the type name from a 'defined' type, handling old and new IDL formats."""
|
||||
defined_value = type_def['defined']
|
||||
defined_value = type_def["defined"]
|
||||
# New format: {'defined': {'name': 'MyType'}}
|
||||
# Old format: {'defined': 'MyType'}
|
||||
return defined_value['name'] if isinstance(defined_value, dict) else defined_value
|
||||
return (
|
||||
defined_value["name"] if isinstance(defined_value, dict) else defined_value
|
||||
)
|
||||
|
||||
def _calculate_defined_type_min_size(self, type_name: str) -> int:
|
||||
"""Calculate minimum size for user-defined types (structs and enums)."""
|
||||
if type_name not in self.types:
|
||||
raise ValueError(f"Unknown defined type: {type_name}")
|
||||
|
||||
type_def = self.types[type_name]['type']
|
||||
|
||||
if type_def['kind'] == 'struct':
|
||||
return sum(self._calculate_type_min_size(field['type']) for field in type_def['fields'])
|
||||
|
||||
if type_def['kind'] == 'enum':
|
||||
|
||||
type_def = self.types[type_name]["type"]
|
||||
|
||||
if type_def["kind"] == "struct":
|
||||
return sum(
|
||||
self._calculate_type_min_size(field["type"])
|
||||
for field in type_def["fields"]
|
||||
)
|
||||
|
||||
if type_def["kind"] == "enum":
|
||||
# The size of an enum is its discriminator plus the size of its LARGEST variant,
|
||||
# as the data layout must accommodate any possible variant.
|
||||
max_variant_size = 0
|
||||
for variant in type_def['variants']:
|
||||
for variant in type_def["variants"]:
|
||||
variant_size = 0
|
||||
for field in variant.get('fields', []):
|
||||
for field in variant.get("fields", []):
|
||||
# A field can be a type string/dict (tuple variant) or a dict with a 'type' key (struct variant)
|
||||
field_type = field['type'] if isinstance(field, dict) else field
|
||||
field_type = field["type"] if isinstance(field, dict) else field
|
||||
variant_size += self._calculate_type_min_size(field_type)
|
||||
max_variant_size = max(max_variant_size, variant_size)
|
||||
return ENUM_DISCRIMINATOR_SIZE + max_variant_size
|
||||
|
||||
raise ValueError(f"Unsupported type kind for size calculation: {type_def['kind']}")
|
||||
raise ValueError(
|
||||
f"Unsupported type kind for size calculation: {type_def['kind']}"
|
||||
)
|
||||
|
||||
def _decode_type(self, data: bytes, offset: int, type_def: str | dict) -> tuple[Any, int]:
|
||||
def _decode_type(
|
||||
self, data: bytes, offset: int, type_def: str | dict
|
||||
) -> tuple[Any, int]:
|
||||
"""Decode a value based on its type definition."""
|
||||
if isinstance(type_def, str):
|
||||
return self._decode_primitive(data, offset, type_def)
|
||||
|
||||
|
||||
if isinstance(type_def, dict):
|
||||
if 'defined' in type_def:
|
||||
if "defined" in type_def:
|
||||
type_name = self._get_defined_type_name(type_def)
|
||||
return self._decode_defined_type(data, offset, type_name)
|
||||
if 'array' in type_def:
|
||||
return self._decode_array(data, offset, type_def['array'])
|
||||
|
||||
if "array" in type_def:
|
||||
return self._decode_array(data, offset, type_def["array"])
|
||||
|
||||
raise ValueError(f"Invalid or unknown type definition for decoding: {type_def}")
|
||||
|
||||
def _decode_array(self, data: bytes, offset: int, array_def: list) -> tuple[list[Any], int]:
|
||||
def _decode_array(
|
||||
self, data: bytes, offset: int, array_def: list
|
||||
) -> tuple[list[Any], int]:
|
||||
"""Decode fixed-size array types."""
|
||||
element_type, array_length = array_def
|
||||
array_data = []
|
||||
@@ -423,20 +461,22 @@ class IDLParser:
|
||||
array_data.append(value)
|
||||
return array_data, offset
|
||||
|
||||
def _decode_primitive(self, data: bytes, offset: int, type_name: str) -> tuple[Any, int]:
|
||||
def _decode_primitive(
|
||||
self, data: bytes, offset: int, type_name: str
|
||||
) -> tuple[Any, int]:
|
||||
"""Decode primitive types."""
|
||||
if type_name not in self._PRIMITIVE_TYPE_INFO:
|
||||
raise ValueError(f"Unknown primitive type: {type_name}")
|
||||
|
||||
if type_name == 'string':
|
||||
length = struct.unpack_from('<I', data, offset)[0]
|
||||
if type_name == "string":
|
||||
length = struct.unpack_from("<I", data, offset)[0]
|
||||
offset += STRING_LENGTH_PREFIX_SIZE
|
||||
value = data[offset:offset + length].decode('utf-8')
|
||||
value = data[offset : offset + length].decode("utf-8")
|
||||
return value, offset + length
|
||||
|
||||
if type_name == 'pubkey':
|
||||
if type_name == "pubkey":
|
||||
end = offset + PUBLIC_KEY_SIZE
|
||||
value = base58.b58encode(data[offset:end]).decode('utf-8')
|
||||
value = base58.b58encode(data[offset:end]).decode("utf-8")
|
||||
return value, end
|
||||
|
||||
# Handle all numeric and bool types from the map
|
||||
@@ -444,47 +484,51 @@ class IDLParser:
|
||||
value = struct.unpack_from(fmt, data, offset)[0]
|
||||
return value, offset + size
|
||||
|
||||
def _decode_defined_type(self, data: bytes, offset: int, type_name: str) -> tuple[dict[str, Any], int]:
|
||||
def _decode_defined_type(
|
||||
self, data: bytes, offset: int, type_name: str
|
||||
) -> tuple[dict[str, Any], int]:
|
||||
"""Decode user-defined types (structs and enums)."""
|
||||
if type_name not in self.types:
|
||||
raise ValueError(f"Unknown defined type: {type_name}")
|
||||
|
||||
type_def = self.types[type_name]['type']
|
||||
|
||||
if type_def['kind'] == 'struct':
|
||||
type_def = self.types[type_name]["type"]
|
||||
|
||||
if type_def["kind"] == "struct":
|
||||
struct_data = {}
|
||||
for field in type_def['fields']:
|
||||
value, offset = self._decode_type(data, offset, field['type'])
|
||||
struct_data[field['name']] = value
|
||||
for field in type_def["fields"]:
|
||||
value, offset = self._decode_type(data, offset, field["type"])
|
||||
struct_data[field["name"]] = value
|
||||
return struct_data, offset
|
||||
|
||||
if type_def['kind'] == 'enum':
|
||||
variant_index = struct.unpack_from('<B', data, offset)[0]
|
||||
|
||||
if type_def["kind"] == "enum":
|
||||
variant_index = struct.unpack_from("<B", data, offset)[0]
|
||||
offset += ENUM_DISCRIMINATOR_SIZE
|
||||
|
||||
variants = type_def['variants']
|
||||
|
||||
variants = type_def["variants"]
|
||||
if variant_index >= len(variants):
|
||||
raise ValueError(f"Invalid enum variant index {variant_index} for type {type_name}")
|
||||
|
||||
raise ValueError(
|
||||
f"Invalid enum variant index {variant_index} for type {type_name}"
|
||||
)
|
||||
|
||||
variant = variants[variant_index]
|
||||
result = {"variant": variant['name']}
|
||||
variant_fields = variant.get('fields', [])
|
||||
result = {"variant": variant["name"]}
|
||||
variant_fields = variant.get("fields", [])
|
||||
|
||||
if variant_fields:
|
||||
# Check if it's a struct variant (fields are dicts) or tuple variant (fields are strings/dicts)
|
||||
if isinstance(variant_fields[0], dict):
|
||||
struct_data = {}
|
||||
for field in variant_fields:
|
||||
value, offset = self._decode_type(data, offset, field['type'])
|
||||
struct_data[field['name']] = value
|
||||
result['data'] = struct_data
|
||||
else: # Tuple variant
|
||||
value, offset = self._decode_type(data, offset, field["type"])
|
||||
struct_data[field["name"]] = value
|
||||
result["data"] = struct_data
|
||||
else: # Tuple variant
|
||||
tuple_data = []
|
||||
for field_type in variant_fields:
|
||||
value, offset = self._decode_type(data, offset, field_type)
|
||||
tuple_data.append(value)
|
||||
result['data'] = tuple_data
|
||||
|
||||
result["data"] = tuple_data
|
||||
|
||||
return result, offset
|
||||
|
||||
raise ValueError(f"Unsupported type kind for decoding: {type_def['kind']}")
|
||||
@@ -501,4 +545,4 @@ def load_idl_parser(idl_path: str, verbose: bool = False) -> IDLParser:
|
||||
Returns:
|
||||
Initialized IDLParser instance
|
||||
"""
|
||||
return IDLParser(idl_path, verbose)
|
||||
return IDLParser(idl_path, verbose)
|
||||
|
||||
Reference in New Issue
Block a user