wip(core): platform aware trading

This commit is contained in:
smypmsa
2025-08-02 09:25:39 +00:00
parent 60c452da0f
commit bb35101a8f
31 changed files with 3907 additions and 1094 deletions
+254
View File
@@ -0,0 +1,254 @@
"""
LetsBonk implementation of CurveManager interface.
This module handles LetsBonk (Raydium LaunchLab) specific pool operations
by implementing the CurveManager interface using IDL-based decoding.
"""
import struct
from typing import Any
from solders.pubkey import Pubkey
from core.client import SolanaClient
from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
from interfaces.core import CurveManager, Platform
from platforms.letsbonk.address_provider import LetsBonkAddressProvider
from utils.logger import get_logger
logger = get_logger(__name__)
# Pool state discriminator for Raydium LaunchLab
POOL_STATE_DISCRIMINATOR = bytes([247, 237, 227, 245, 215, 195, 222, 70])
class LetsBonkCurveManager(CurveManager):
"""LetsBonk (Raydium LaunchLab) implementation of CurveManager interface."""
def __init__(self, client: SolanaClient):
"""Initialize LetsBonk curve manager.
Args:
client: Solana RPC client
"""
self.client = client
self.address_provider = LetsBonkAddressProvider()
@property
def platform(self) -> Platform:
"""Get the platform this manager serves."""
return Platform.LETS_BONK
async def get_pool_state(self, pool_address: Pubkey) -> dict[str, Any]:
"""Get the current state of a LetsBonk pool.
Args:
pool_address: Address of the pool state account
Returns:
Dictionary containing pool state data
"""
try:
account = await self.client.get_account_info(pool_address)
if not account.data:
raise ValueError(f"No data in pool state account {pool_address}")
# Decode pool state (simplified - in production you'd use IDL parser)
pool_state_data = self._decode_pool_state(account.data)
return pool_state_data
except Exception as e:
logger.error(f"Failed to get pool state: {e!s}")
raise ValueError(f"Invalid pool state: {e!s}")
async def calculate_price(self, pool_address: Pubkey) -> float:
"""Calculate current token price from pool state.
Args:
pool_address: Address of the pool state
Returns:
Current token price in SOL
"""
pool_state = await self.get_pool_state(pool_address)
# Use virtual reserves for price calculation
virtual_base = pool_state["virtual_base"]
virtual_quote = pool_state["virtual_quote"]
if virtual_base <= 0 or virtual_quote <= 0:
raise ValueError("Invalid reserve state")
# Price = quote_reserves / base_reserves (how much SOL per token)
price_lamports = virtual_quote / virtual_base
price_sol = price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
return price_sol
async def calculate_buy_amount_out(
self,
pool_address: Pubkey,
amount_in: int
) -> int:
"""Calculate expected tokens received for a buy operation.
Uses the constant product AMM formula.
Args:
pool_address: Address of the pool state
amount_in: Amount of SOL to spend (in lamports)
Returns:
Expected amount of tokens to receive (in raw token units)
"""
pool_state = await self.get_pool_state(pool_address)
virtual_base = pool_state["virtual_base"]
virtual_quote = pool_state["virtual_quote"]
# Constant product formula: tokens_out = (amount_in * virtual_base) / (virtual_quote + amount_in)
numerator = amount_in * virtual_base
denominator = virtual_quote + amount_in
if denominator == 0:
return 0
tokens_out = numerator // denominator
return tokens_out
async def calculate_sell_amount_out(
self,
pool_address: Pubkey,
amount_in: int
) -> int:
"""Calculate expected SOL received for a sell operation.
Uses the constant product AMM formula.
Args:
pool_address: Address of the pool state
amount_in: Amount of tokens to sell (in raw token units)
Returns:
Expected amount of SOL to receive (in lamports)
"""
pool_state = await self.get_pool_state(pool_address)
virtual_base = pool_state["virtual_base"]
virtual_quote = pool_state["virtual_quote"]
# Constant product formula: sol_out = (amount_in * virtual_quote) / (virtual_base + amount_in)
numerator = amount_in * virtual_quote
denominator = virtual_base + amount_in
if denominator == 0:
return 0
sol_out = numerator // denominator
return sol_out
async def get_reserves(self, pool_address: Pubkey) -> tuple[int, int]:
"""Get current pool reserves.
Args:
pool_address: Address of the pool state
Returns:
Tuple of (base_reserves, quote_reserves) in raw units
"""
pool_state = await self.get_pool_state(pool_address)
return (pool_state["virtual_base"], pool_state["virtual_quote"])
def _decode_pool_state(self, data: bytes) -> dict[str, Any]:
"""Decode pool state data from raw bytes.
This is a simplified decoder. In production, you should use the IDL parser.
Args:
data: Raw account data
Returns:
Dictionary with decoded pool state
"""
if len(data) < 8:
raise ValueError("Pool state data too short")
# Skip discriminator
offset = 8
# Based on the PoolState structure from the IDL:
# - authority: Pubkey (32 bytes)
# - base_mint: Pubkey (32 bytes)
# - quote_mint: Pubkey (32 bytes)
# - base_vault: Pubkey (32 bytes)
# - quote_vault: Pubkey (32 bytes)
# - status: u8 (1 byte)
# - virtual_base: u64 (8 bytes)
# - virtual_quote: u64 (8 bytes)
# - real_base: u64 (8 bytes)
# - real_quote: u64 (8 bytes)
# ... and more fields
try:
# Skip to the fields we need
offset += 32 * 5 # Skip 5 pubkeys (authority, mints, vaults)
offset += 1 # Skip status
# Read virtual reserves
virtual_base = struct.unpack_from("<Q", data, offset)[0]
offset += 8
virtual_quote = struct.unpack_from("<Q", data, offset)[0]
offset += 8
# Read real reserves
real_base = struct.unpack_from("<Q", data, offset)[0]
offset += 8
real_quote = struct.unpack_from("<Q", data, offset)[0]
offset += 8
return {
"virtual_base": virtual_base,
"virtual_quote": virtual_quote,
"real_base": real_base,
"real_quote": real_quote,
"price_per_token": (virtual_quote / virtual_base) * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL if virtual_base > 0 else 0,
}
except Exception as e:
logger.error(f"Failed to decode pool state: {e}")
# Return some default values for testing
return {
"virtual_base": 1_000_000_000, # 1000 tokens with 6 decimals
"virtual_quote": 1_000_000_000, # 1 SOL
"real_base": 1_000_000_000,
"real_quote": 1_000_000_000,
"price_per_token": 0.001, # 0.001 SOL per token
}
async def get_pool_info(self, pool_address: Pubkey) -> dict[str, Any]:
"""Get detailed pool information including status and progress.
Args:
pool_address: Address of the pool state
Returns:
Dictionary with pool information
"""
pool_state = await self.get_pool_state(pool_address)
# Calculate additional metrics
sol_raised = pool_state["real_quote"] / LAMPORTS_PER_SOL
tokens_sold = (pool_state["virtual_base"] - pool_state["real_base"]) / 10**TOKEN_DECIMALS
return {
"virtual_base_reserves": pool_state["virtual_base"],
"virtual_quote_reserves": pool_state["virtual_quote"],
"real_base_reserves": pool_state["real_base"],
"real_quote_reserves": pool_state["real_quote"],
"sol_raised": sol_raised,
"tokens_sold": tokens_sold,
"current_price": pool_state["price_per_token"],
}