mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-15 00:08:04 +00:00
fix(pump): fix curve management
This commit is contained in:
@@ -1,165 +0,0 @@
|
|||||||
"""
|
|
||||||
Pump.fun bonding curve manager for price calculations and curve state management.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import struct
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from solders.pubkey import Pubkey
|
|
||||||
|
|
||||||
from core.client import SolanaClient
|
|
||||||
from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
|
|
||||||
from utils.logger import get_logger
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
# Bonding curve discriminator
|
|
||||||
CURVE_DISCRIMINATOR = bytes([23, 203, 71, 8, 209, 70, 227, 3])
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class BondingCurveState:
|
|
||||||
"""Represents the state of a pump.fun bonding curve."""
|
|
||||||
|
|
||||||
virtual_token_reserves: int
|
|
||||||
virtual_sol_reserves: int
|
|
||||||
real_token_reserves: int
|
|
||||||
real_sol_reserves: int
|
|
||||||
token_total_supply: int
|
|
||||||
complete: bool
|
|
||||||
creator: Pubkey
|
|
||||||
|
|
||||||
@property
|
|
||||||
def token_reserves(self) -> float:
|
|
||||||
"""Token reserves in decimal form."""
|
|
||||||
return self.virtual_token_reserves / 10**TOKEN_DECIMALS
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sol_reserves(self) -> float:
|
|
||||||
"""SOL reserves in decimal form."""
|
|
||||||
return self.virtual_sol_reserves / LAMPORTS_PER_SOL
|
|
||||||
|
|
||||||
def calculate_price(self) -> float:
|
|
||||||
"""Calculate current token price in SOL."""
|
|
||||||
if self.virtual_token_reserves <= 0:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
# Price = sol_reserves / token_reserves
|
|
||||||
price_lamports = self.virtual_sol_reserves / self.virtual_token_reserves
|
|
||||||
return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
|
|
||||||
|
|
||||||
|
|
||||||
class BondingCurveManager:
|
|
||||||
"""Manages pump.fun bonding curve operations."""
|
|
||||||
|
|
||||||
def __init__(self, client: SolanaClient):
|
|
||||||
"""Initialize bonding curve manager.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
client: Solana RPC client
|
|
||||||
"""
|
|
||||||
self.client = client
|
|
||||||
|
|
||||||
async def get_curve_state(self, curve_address: Pubkey) -> BondingCurveState:
|
|
||||||
"""Get the current state of a bonding curve.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
curve_address: Address of the bonding curve
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
BondingCurveState with current curve data
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If curve data is invalid or inaccessible
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
account = await self.client.get_account_info(curve_address)
|
|
||||||
if not account.data:
|
|
||||||
raise ValueError(f"No data in bonding curve account {curve_address}")
|
|
||||||
|
|
||||||
curve_state = self._decode_curve_state(account.data)
|
|
||||||
return curve_state
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to get curve state: {e!s}")
|
|
||||||
raise ValueError(f"Invalid bonding curve state: {e!s}")
|
|
||||||
|
|
||||||
async def calculate_price(self, curve_address: Pubkey) -> float:
|
|
||||||
"""Calculate current token price from bonding curve.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
curve_address: Address of the bonding curve
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Current token price in SOL
|
|
||||||
"""
|
|
||||||
curve_state = await self.get_curve_state(curve_address)
|
|
||||||
return curve_state.calculate_price()
|
|
||||||
|
|
||||||
def _decode_curve_state(self, data: bytes) -> BondingCurveState:
|
|
||||||
"""Decode bonding curve state from raw account data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data: Raw account data
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Decoded BondingCurveState
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If data format is invalid
|
|
||||||
"""
|
|
||||||
if len(data) < 8:
|
|
||||||
raise ValueError("Curve data too short")
|
|
||||||
|
|
||||||
# Check discriminator
|
|
||||||
if not data.startswith(CURVE_DISCRIMINATOR):
|
|
||||||
raise ValueError("Invalid curve discriminator")
|
|
||||||
|
|
||||||
offset = 8
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Decode based on pump.fun BondingCurve structure:
|
|
||||||
# - virtual_token_reserves: u64 (8 bytes)
|
|
||||||
# - virtual_sol_reserves: u64 (8 bytes)
|
|
||||||
# - real_token_reserves: u64 (8 bytes)
|
|
||||||
# - real_sol_reserves: u64 (8 bytes)
|
|
||||||
# - token_total_supply: u64 (8 bytes)
|
|
||||||
# - complete: bool (1 byte)
|
|
||||||
# - padding: 7 bytes
|
|
||||||
# - creator: Pubkey (32 bytes)
|
|
||||||
|
|
||||||
virtual_token_reserves = struct.unpack_from("<Q", data, offset)[0]
|
|
||||||
offset += 8
|
|
||||||
|
|
||||||
virtual_sol_reserves = struct.unpack_from("<Q", data, offset)[0]
|
|
||||||
offset += 8
|
|
||||||
|
|
||||||
real_token_reserves = struct.unpack_from("<Q", data, offset)[0]
|
|
||||||
offset += 8
|
|
||||||
|
|
||||||
real_sol_reserves = struct.unpack_from("<Q", data, offset)[0]
|
|
||||||
offset += 8
|
|
||||||
|
|
||||||
token_total_supply = struct.unpack_from("<Q", data, offset)[0]
|
|
||||||
offset += 8
|
|
||||||
|
|
||||||
complete = bool(struct.unpack_from("<B", data, offset)[0])
|
|
||||||
offset += 1
|
|
||||||
|
|
||||||
# Skip padding
|
|
||||||
offset += 7
|
|
||||||
|
|
||||||
creator = Pubkey.from_bytes(data[offset:offset + 32])
|
|
||||||
|
|
||||||
return BondingCurveState(
|
|
||||||
virtual_token_reserves=virtual_token_reserves,
|
|
||||||
virtual_sol_reserves=virtual_sol_reserves,
|
|
||||||
real_token_reserves=real_token_reserves,
|
|
||||||
real_sol_reserves=real_sol_reserves,
|
|
||||||
token_total_supply=token_total_supply,
|
|
||||||
complete=complete,
|
|
||||||
creator=creator,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(f"Failed to decode curve state: {e}")
|
|
||||||
@@ -2,17 +2,56 @@
|
|||||||
Pump.Fun implementation of CurveManager interface.
|
Pump.Fun implementation of CurveManager interface.
|
||||||
|
|
||||||
This module handles pump.fun-specific bonding curve operations
|
This module handles pump.fun-specific bonding curve operations
|
||||||
by implementing the CurveManager interface.
|
by implementing the CurveManager interface with all curve logic self-contained.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import struct
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from solders.pubkey import Pubkey
|
from solders.pubkey import Pubkey
|
||||||
|
|
||||||
from core.client import SolanaClient
|
from core.client import SolanaClient
|
||||||
from core.curve import BondingCurveManager
|
|
||||||
from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
|
from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
|
||||||
from interfaces.core import CurveManager, Platform
|
from interfaces.core import CurveManager, Platform
|
||||||
|
from utils.logger import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# Bonding curve discriminator for pump.fun
|
||||||
|
CURVE_DISCRIMINATOR = bytes([23, 203, 71, 8, 209, 70, 227, 3])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BondingCurveState:
|
||||||
|
"""Represents the state of a pump.fun bonding curve."""
|
||||||
|
|
||||||
|
virtual_token_reserves: int
|
||||||
|
virtual_sol_reserves: int
|
||||||
|
real_token_reserves: int
|
||||||
|
real_sol_reserves: int
|
||||||
|
token_total_supply: int
|
||||||
|
complete: bool
|
||||||
|
creator: Pubkey
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token_reserves(self) -> float:
|
||||||
|
"""Token reserves in decimal form."""
|
||||||
|
return self.virtual_token_reserves / 10**TOKEN_DECIMALS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sol_reserves(self) -> float:
|
||||||
|
"""SOL reserves in decimal form."""
|
||||||
|
return self.virtual_sol_reserves / LAMPORTS_PER_SOL
|
||||||
|
|
||||||
|
def calculate_price(self) -> float:
|
||||||
|
"""Calculate current token price in SOL."""
|
||||||
|
if self.virtual_token_reserves <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
# Price = sol_reserves / token_reserves
|
||||||
|
price_lamports = self.virtual_sol_reserves / self.virtual_token_reserves
|
||||||
|
return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
|
||||||
|
|
||||||
|
|
||||||
class PumpFunCurveManager(CurveManager):
|
class PumpFunCurveManager(CurveManager):
|
||||||
@@ -25,7 +64,6 @@ class PumpFunCurveManager(CurveManager):
|
|||||||
client: Solana RPC client
|
client: Solana RPC client
|
||||||
"""
|
"""
|
||||||
self.client = client
|
self.client = client
|
||||||
self.bonding_curve_manager = BondingCurveManager(client)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def platform(self) -> Platform:
|
def platform(self) -> Platform:
|
||||||
@@ -41,22 +79,31 @@ class PumpFunCurveManager(CurveManager):
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary containing bonding curve state data
|
Dictionary containing bonding curve state data
|
||||||
"""
|
"""
|
||||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
try:
|
||||||
|
account = await self.client.get_account_info(pool_address)
|
||||||
|
if not account.data:
|
||||||
|
raise ValueError(f"No data in bonding curve account {pool_address}")
|
||||||
|
|
||||||
return {
|
curve_state = self._decode_curve_state(account.data)
|
||||||
"virtual_token_reserves": curve_state.virtual_token_reserves,
|
|
||||||
"virtual_sol_reserves": curve_state.virtual_sol_reserves,
|
|
||||||
"real_token_reserves": curve_state.real_token_reserves,
|
|
||||||
"real_sol_reserves": curve_state.real_sol_reserves,
|
|
||||||
"token_total_supply": curve_state.token_total_supply,
|
|
||||||
"complete": curve_state.complete,
|
|
||||||
"creator": str(curve_state.creator),
|
|
||||||
|
|
||||||
# Calculated fields for convenience
|
return {
|
||||||
"price_per_token": curve_state.calculate_price(),
|
"virtual_token_reserves": curve_state.virtual_token_reserves,
|
||||||
"token_reserves_decimal": curve_state.token_reserves,
|
"virtual_sol_reserves": curve_state.virtual_sol_reserves,
|
||||||
"sol_reserves_decimal": curve_state.sol_reserves,
|
"real_token_reserves": curve_state.real_token_reserves,
|
||||||
}
|
"real_sol_reserves": curve_state.real_sol_reserves,
|
||||||
|
"token_total_supply": curve_state.token_total_supply,
|
||||||
|
"complete": curve_state.complete,
|
||||||
|
"creator": str(curve_state.creator),
|
||||||
|
|
||||||
|
# Calculated fields for convenience
|
||||||
|
"price_per_token": curve_state.calculate_price(),
|
||||||
|
"token_reserves_decimal": curve_state.token_reserves,
|
||||||
|
"sol_reserves_decimal": curve_state.sol_reserves,
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get curve state: {e!s}")
|
||||||
|
raise ValueError(f"Invalid bonding curve state: {e!s}")
|
||||||
|
|
||||||
async def calculate_price(self, pool_address: Pubkey) -> float:
|
async def calculate_price(self, pool_address: Pubkey) -> float:
|
||||||
"""Calculate current token price from bonding curve state.
|
"""Calculate current token price from bonding curve state.
|
||||||
@@ -67,7 +114,8 @@ class PumpFunCurveManager(CurveManager):
|
|||||||
Returns:
|
Returns:
|
||||||
Current token price in SOL
|
Current token price in SOL
|
||||||
"""
|
"""
|
||||||
return await self.bonding_curve_manager.calculate_price(pool_address)
|
curve_state = await self._get_curve_state_object(pool_address)
|
||||||
|
return curve_state.calculate_price()
|
||||||
|
|
||||||
async def calculate_buy_amount_out(
|
async def calculate_buy_amount_out(
|
||||||
self,
|
self,
|
||||||
@@ -85,7 +133,7 @@ class PumpFunCurveManager(CurveManager):
|
|||||||
Returns:
|
Returns:
|
||||||
Expected amount of tokens to receive (in raw token units)
|
Expected amount of tokens to receive (in raw token units)
|
||||||
"""
|
"""
|
||||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
curve_state = await self._get_curve_state_object(pool_address)
|
||||||
|
|
||||||
# Use virtual reserves for bonding curve calculation
|
# Use virtual reserves for bonding curve calculation
|
||||||
# Formula: tokens_out = (amount_in * virtual_token_reserves) / (virtual_sol_reserves + amount_in)
|
# Formula: tokens_out = (amount_in * virtual_token_reserves) / (virtual_sol_reserves + amount_in)
|
||||||
@@ -114,7 +162,7 @@ class PumpFunCurveManager(CurveManager):
|
|||||||
Returns:
|
Returns:
|
||||||
Expected amount of SOL to receive (in lamports)
|
Expected amount of SOL to receive (in lamports)
|
||||||
"""
|
"""
|
||||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
curve_state = await self._get_curve_state_object(pool_address)
|
||||||
|
|
||||||
# Use virtual reserves for bonding curve calculation
|
# Use virtual reserves for bonding curve calculation
|
||||||
# Formula: sol_out = (amount_in * virtual_sol_reserves) / (virtual_token_reserves + amount_in)
|
# Formula: sol_out = (amount_in * virtual_sol_reserves) / (virtual_token_reserves + amount_in)
|
||||||
@@ -136,14 +184,106 @@ class PumpFunCurveManager(CurveManager):
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (token_reserves, sol_reserves) in raw units
|
Tuple of (token_reserves, sol_reserves) in raw units
|
||||||
"""
|
"""
|
||||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
curve_state = await self._get_curve_state_object(pool_address)
|
||||||
return (curve_state.virtual_token_reserves, curve_state.virtual_sol_reserves)
|
return (curve_state.virtual_token_reserves, curve_state.virtual_sol_reserves)
|
||||||
|
|
||||||
|
async def _get_curve_state_object(self, curve_address: Pubkey) -> BondingCurveState:
|
||||||
|
"""Get the current state of a bonding curve as a BondingCurveState object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
curve_address: Address of the bonding curve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BondingCurveState with current curve data
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If curve data is invalid or inaccessible
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
account = await self.client.get_account_info(curve_address)
|
||||||
|
if not account.data:
|
||||||
|
raise ValueError(f"No data in bonding curve account {curve_address}")
|
||||||
|
|
||||||
|
curve_state = self._decode_curve_state(account.data)
|
||||||
|
return curve_state
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get curve state: {e!s}")
|
||||||
|
raise ValueError(f"Invalid bonding curve state: {e!s}")
|
||||||
|
|
||||||
|
def _decode_curve_state(self, data: bytes) -> BondingCurveState:
|
||||||
|
"""Decode bonding curve state from raw account data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data: Raw account data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decoded BondingCurveState
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If data format is invalid
|
||||||
|
"""
|
||||||
|
if len(data) < 8:
|
||||||
|
raise ValueError("Curve data too short")
|
||||||
|
|
||||||
|
# Check discriminator
|
||||||
|
if not data.startswith(CURVE_DISCRIMINATOR):
|
||||||
|
raise ValueError("Invalid curve discriminator")
|
||||||
|
|
||||||
|
offset = 8
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Decode based on pump.fun BondingCurve structure:
|
||||||
|
# - virtual_token_reserves: u64 (8 bytes)
|
||||||
|
# - virtual_sol_reserves: u64 (8 bytes)
|
||||||
|
# - real_token_reserves: u64 (8 bytes)
|
||||||
|
# - real_sol_reserves: u64 (8 bytes)
|
||||||
|
# - token_total_supply: u64 (8 bytes)
|
||||||
|
# - complete: bool (1 byte)
|
||||||
|
# - padding: 7 bytes
|
||||||
|
# - creator: Pubkey (32 bytes)
|
||||||
|
|
||||||
|
virtual_token_reserves = struct.unpack_from("<Q", data, offset)[0]
|
||||||
|
offset += 8
|
||||||
|
|
||||||
|
virtual_sol_reserves = struct.unpack_from("<Q", data, offset)[0]
|
||||||
|
offset += 8
|
||||||
|
|
||||||
|
real_token_reserves = struct.unpack_from("<Q", data, offset)[0]
|
||||||
|
offset += 8
|
||||||
|
|
||||||
|
real_sol_reserves = struct.unpack_from("<Q", data, offset)[0]
|
||||||
|
offset += 8
|
||||||
|
|
||||||
|
token_total_supply = struct.unpack_from("<Q", data, offset)[0]
|
||||||
|
offset += 8
|
||||||
|
|
||||||
|
complete = bool(struct.unpack_from("<B", data, offset)[0])
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
# Skip padding
|
||||||
|
offset += 7
|
||||||
|
|
||||||
|
creator = Pubkey.from_bytes(data[offset:offset + 32])
|
||||||
|
|
||||||
|
return BondingCurveState(
|
||||||
|
virtual_token_reserves=virtual_token_reserves,
|
||||||
|
virtual_sol_reserves=virtual_sol_reserves,
|
||||||
|
real_token_reserves=real_token_reserves,
|
||||||
|
real_sol_reserves=real_sol_reserves,
|
||||||
|
token_total_supply=token_total_supply,
|
||||||
|
complete=complete,
|
||||||
|
creator=creator,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
raise ValueError(f"Failed to decode curve state: {e}")
|
||||||
|
|
||||||
|
# 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.
|
"""Calculate the expected token amount for a given SOL input.
|
||||||
|
|
||||||
This is a convenience method that wraps calculate_buy_amount_out
|
This is a convenience method that converts between decimal and raw units.
|
||||||
and converts between decimal and raw units.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pool_address: Address of the bonding curve
|
pool_address: Address of the bonding curve
|
||||||
@@ -159,8 +299,7 @@ class PumpFunCurveManager(CurveManager):
|
|||||||
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.
|
"""Calculate the expected SOL amount for a given token input.
|
||||||
|
|
||||||
This is a convenience method that wraps calculate_sell_amount_out
|
This is a convenience method that converts between decimal and raw units.
|
||||||
and converts between decimal and raw units.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pool_address: Address of the bonding curve
|
pool_address: Address of the bonding curve
|
||||||
@@ -182,7 +321,7 @@ class PumpFunCurveManager(CurveManager):
|
|||||||
Returns:
|
Returns:
|
||||||
True if curve is complete, False otherwise
|
True if curve is complete, False otherwise
|
||||||
"""
|
"""
|
||||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
curve_state = await self._get_curve_state_object(pool_address)
|
||||||
return curve_state.complete
|
return curve_state.complete
|
||||||
|
|
||||||
async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]:
|
async def get_curve_progress(self, pool_address: Pubkey) -> dict[str, Any]:
|
||||||
@@ -194,7 +333,7 @@ class PumpFunCurveManager(CurveManager):
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary with progress information
|
Dictionary with progress information
|
||||||
"""
|
"""
|
||||||
curve_state = await self.bonding_curve_manager.get_curve_state(pool_address)
|
curve_state = await self._get_curve_state_object(pool_address)
|
||||||
|
|
||||||
# Calculate progress based on SOL raised vs target
|
# Calculate progress based on SOL raised vs target
|
||||||
# This is approximate since the exact target isn't stored in the curve state
|
# This is approximate since the exact target isn't stored in the curve state
|
||||||
|
|||||||
Reference in New Issue
Block a user