mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-08-04 19:27:44 +00:00
updated code structure
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
Solana client abstraction for blockchain operations.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from solana.rpc.async_api import AsyncClient
|
||||
from solana.rpc.commitment import Confirmed
|
||||
from solana.rpc.types import TxOpts
|
||||
from solana.transaction import Transaction
|
||||
from solders.instruction import Instruction
|
||||
from solders.keypair import Keypair
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from src.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SolanaClient:
|
||||
"""Abstraction for Solana RPC client operations."""
|
||||
|
||||
def __init__(self, rpc_endpoint: str):
|
||||
"""Initialize Solana client with RPC endpoint.
|
||||
|
||||
Args:
|
||||
rpc_endpoint: URL of the Solana RPC endpoint
|
||||
"""
|
||||
self.rpc_endpoint = rpc_endpoint
|
||||
self._client = None
|
||||
|
||||
async def get_client(self) -> AsyncClient:
|
||||
"""Get or create the AsyncClient instance.
|
||||
|
||||
Returns:
|
||||
AsyncClient instance
|
||||
"""
|
||||
if self._client is None:
|
||||
self._client = AsyncClient(self.rpc_endpoint)
|
||||
return self._client
|
||||
|
||||
async def close(self):
|
||||
"""Close the client connection if open."""
|
||||
if self._client:
|
||||
await self._client.close()
|
||||
self._client = None
|
||||
|
||||
async def get_account_info(self, pubkey: Pubkey) -> Dict[str, Any]:
|
||||
"""Get account info from the blockchain.
|
||||
|
||||
Args:
|
||||
pubkey: Public key of the account
|
||||
|
||||
Returns:
|
||||
Account info response
|
||||
|
||||
Raises:
|
||||
ValueError: If account doesn't exist or has no data
|
||||
"""
|
||||
client = await self.get_client()
|
||||
response = await client.get_account_info(pubkey)
|
||||
if not response.value:
|
||||
raise ValueError(f"Account {pubkey} not found")
|
||||
return response.value
|
||||
|
||||
async def get_token_account_balance(self, token_account: Pubkey) -> int:
|
||||
"""Get token balance for an account.
|
||||
|
||||
Args:
|
||||
token_account: Token account address
|
||||
|
||||
Returns:
|
||||
Token balance as integer
|
||||
"""
|
||||
client = await self.get_client()
|
||||
response = await client.get_token_account_balance(token_account)
|
||||
if response.value:
|
||||
return int(response.value.amount)
|
||||
return 0
|
||||
|
||||
async def get_latest_blockhash(self) -> str:
|
||||
"""Get the latest blockhash.
|
||||
|
||||
Returns:
|
||||
Recent blockhash as string
|
||||
"""
|
||||
client = await self.get_client()
|
||||
response = await client.get_latest_blockhash()
|
||||
return response.value.blockhash
|
||||
|
||||
async def send_transaction(
|
||||
self,
|
||||
transaction: Transaction,
|
||||
signer: Keypair,
|
||||
skip_preflight: bool = True,
|
||||
max_retries: int = 3,
|
||||
) -> str:
|
||||
"""Send a transaction to the network.
|
||||
|
||||
Args:
|
||||
transaction: Prepared transaction
|
||||
signer: Transaction signer
|
||||
skip_preflight: Whether to skip preflight checks
|
||||
max_retries: Maximum number of sending attempts
|
||||
|
||||
Returns:
|
||||
Transaction signature
|
||||
|
||||
Raises:
|
||||
Exception: If transaction fails after all retries
|
||||
"""
|
||||
client = await self.get_client()
|
||||
|
||||
# Ensure transaction has a recent blockhash
|
||||
if not transaction.recent_blockhash:
|
||||
blockhash = await self.get_latest_blockhash()
|
||||
transaction.recent_blockhash = blockhash
|
||||
|
||||
# Attempt to send with retries
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
tx_opts = TxOpts(
|
||||
skip_preflight=skip_preflight, preflight_commitment=Confirmed
|
||||
)
|
||||
response = await client.send_transaction(
|
||||
transaction, signer, opts=tx_opts
|
||||
)
|
||||
return response.value
|
||||
|
||||
except Exception as e:
|
||||
if attempt == max_retries - 1:
|
||||
logger.error(
|
||||
f"Failed to send transaction after {max_retries} attempts"
|
||||
)
|
||||
raise
|
||||
|
||||
wait_time = 2**attempt
|
||||
logger.warning(
|
||||
f"Transaction attempt {attempt + 1} failed: {str(e)}, retrying in {wait_time}s"
|
||||
)
|
||||
await asyncio.sleep(wait_time)
|
||||
|
||||
async def confirm_transaction(
|
||||
self, signature: str, commitment: str = "confirmed"
|
||||
) -> bool:
|
||||
"""Wait for transaction confirmation.
|
||||
|
||||
Args:
|
||||
signature: Transaction signature
|
||||
commitment: Confirmation commitment level
|
||||
|
||||
Returns:
|
||||
Whether transaction was confirmed
|
||||
"""
|
||||
client = await self.get_client()
|
||||
try:
|
||||
await client.confirm_transaction(signature, commitment=commitment)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to confirm transaction {signature}: {str(e)}")
|
||||
return False
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
Bonding curve operations for pump.fun tokens.
|
||||
"""
|
||||
|
||||
import struct
|
||||
from typing import Final
|
||||
|
||||
from construct import Flag, Int64ul, Struct
|
||||
from solana.rpc.async_api import AsyncClient
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from src.core.client import SolanaClient
|
||||
from src.core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
|
||||
from src.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Discriminator for the bonding curve account
|
||||
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 6966180631402821399)
|
||||
|
||||
|
||||
class BondingCurveState:
|
||||
"""Represents the state of a pump.fun bonding curve."""
|
||||
|
||||
_STRUCT = Struct(
|
||||
"virtual_token_reserves" / Int64ul,
|
||||
"virtual_sol_reserves" / Int64ul,
|
||||
"real_token_reserves" / Int64ul,
|
||||
"real_sol_reserves" / Int64ul,
|
||||
"token_total_supply" / Int64ul,
|
||||
"complete" / Flag,
|
||||
)
|
||||
|
||||
def __init__(self, data: bytes) -> None:
|
||||
"""Parse bonding curve data.
|
||||
|
||||
Args:
|
||||
data: Raw account data
|
||||
|
||||
Raises:
|
||||
ValueError: If data cannot be parsed
|
||||
"""
|
||||
if data[:8] != EXPECTED_DISCRIMINATOR:
|
||||
raise ValueError("Invalid curve state discriminator")
|
||||
|
||||
parsed = self._STRUCT.parse(data[8:])
|
||||
self.__dict__.update(parsed)
|
||||
|
||||
def calculate_price(self) -> float:
|
||||
"""Calculate token price in SOL.
|
||||
|
||||
Returns:
|
||||
Token price in SOL
|
||||
|
||||
Raises:
|
||||
ValueError: If reserve state is invalid
|
||||
"""
|
||||
if self.virtual_token_reserves <= 0 or self.virtual_sol_reserves <= 0:
|
||||
raise ValueError("Invalid reserve state")
|
||||
|
||||
return (self.virtual_sol_reserves / LAMPORTS_PER_SOL) / (
|
||||
self.virtual_token_reserves / 10**TOKEN_DECIMALS
|
||||
)
|
||||
|
||||
@property
|
||||
def token_reserves(self) -> float:
|
||||
"""Get token reserves in decimal form."""
|
||||
return self.virtual_token_reserves / 10**TOKEN_DECIMALS
|
||||
|
||||
@property
|
||||
def sol_reserves(self) -> float:
|
||||
"""Get SOL reserves in decimal form."""
|
||||
return self.virtual_sol_reserves / LAMPORTS_PER_SOL
|
||||
|
||||
|
||||
class BondingCurveManager:
|
||||
"""Manager for bonding curve operations."""
|
||||
|
||||
def __init__(self, client: SolanaClient):
|
||||
"""Initialize with Solana client.
|
||||
|
||||
Args:
|
||||
client: Solana client for RPC calls
|
||||
"""
|
||||
self.client = client
|
||||
|
||||
async def get_curve_state(self, curve_address: Pubkey) -> BondingCurveState:
|
||||
"""Get the state of a bonding curve.
|
||||
|
||||
Args:
|
||||
curve_address: Address of the bonding curve account
|
||||
|
||||
Returns:
|
||||
Bonding curve state
|
||||
|
||||
Raises:
|
||||
ValueError: If curve data is invalid
|
||||
"""
|
||||
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}")
|
||||
|
||||
return BondingCurveState(account.data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get curve state: {str(e)}")
|
||||
raise ValueError(f"Invalid curve state: {str(e)}")
|
||||
|
||||
async def calculate_price(self, curve_address: Pubkey) -> float:
|
||||
"""Calculate the current price of a token.
|
||||
|
||||
Args:
|
||||
curve_address: Address of the bonding curve account
|
||||
|
||||
Returns:
|
||||
Token price in SOL
|
||||
"""
|
||||
curve_state = await self.get_curve_state(curve_address)
|
||||
return curve_state.calculate_price()
|
||||
|
||||
async def calculate_expected_tokens(
|
||||
self, curve_address: Pubkey, sol_amount: float
|
||||
) -> float:
|
||||
"""Calculate the expected token amount for a given SOL input.
|
||||
|
||||
Args:
|
||||
curve_address: Address of the bonding curve account
|
||||
sol_amount: Amount of SOL to spend
|
||||
|
||||
Returns:
|
||||
Expected token amount
|
||||
"""
|
||||
curve_state = await self.get_curve_state(curve_address)
|
||||
price = curve_state.calculate_price()
|
||||
return sol_amount / price
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
System and program addresses for Solana and pump.fun interactions.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
LAMPORTS_PER_SOL: Final[int] = 1_000_000_000
|
||||
TOKEN_DECIMALS: Final[int] = 6
|
||||
|
||||
|
||||
@dataclass
|
||||
class SystemAddresses:
|
||||
"""System-level Solana addresses."""
|
||||
|
||||
PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111")
|
||||
TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
)
|
||||
ASSOCIATED_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||
)
|
||||
RENT: Final[Pubkey] = Pubkey.from_string(
|
||||
"SysvarRent111111111111111111111111111111111"
|
||||
)
|
||||
SOL: Final[Pubkey] = Pubkey.from_string(
|
||||
"So11111111111111111111111111111111111111112"
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PumpAddresses:
|
||||
"""Pump.fun program addresses."""
|
||||
|
||||
PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
|
||||
)
|
||||
GLOBAL: Final[Pubkey] = Pubkey.from_string(
|
||||
"4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"
|
||||
)
|
||||
EVENT_AUTHORITY: Final[Pubkey] = Pubkey.from_string(
|
||||
"Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"
|
||||
)
|
||||
FEE: Final[Pubkey] = Pubkey.from_string(
|
||||
"CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"
|
||||
)
|
||||
LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string(
|
||||
"39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"
|
||||
)
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Wallet management for Solana transactions.
|
||||
"""
|
||||
|
||||
import base58
|
||||
from solders.keypair import Keypair
|
||||
from solders.pubkey import Pubkey
|
||||
from spl.token.instructions import get_associated_token_address
|
||||
|
||||
|
||||
class Wallet:
|
||||
"""Manages a Solana wallet for trading operations."""
|
||||
|
||||
def __init__(self, private_key: str):
|
||||
"""Initialize wallet from private key.
|
||||
|
||||
Args:
|
||||
private_key: Base58 encoded private key
|
||||
"""
|
||||
self._private_key = private_key
|
||||
self._keypair = self._load_keypair(private_key)
|
||||
|
||||
@property
|
||||
def pubkey(self) -> Pubkey:
|
||||
"""Get the public key of the wallet."""
|
||||
return self._keypair.pubkey()
|
||||
|
||||
@property
|
||||
def keypair(self) -> Keypair:
|
||||
"""Get the keypair for signing transactions."""
|
||||
return self._keypair
|
||||
|
||||
def get_associated_token_address(self, mint: Pubkey) -> Pubkey:
|
||||
"""Get the associated token account address for a mint.
|
||||
|
||||
Args:
|
||||
mint: Token mint address
|
||||
|
||||
Returns:
|
||||
Associated token account address
|
||||
"""
|
||||
return get_associated_token_address(self.pubkey, mint)
|
||||
|
||||
@staticmethod
|
||||
def _load_keypair(private_key: str) -> Keypair:
|
||||
"""Load keypair from private key.
|
||||
|
||||
Args:
|
||||
private_key: Base58 encoded private key
|
||||
|
||||
Returns:
|
||||
Solana keypair
|
||||
"""
|
||||
private_key_bytes = base58.b58decode(private_key)
|
||||
return Keypair.from_bytes(private_key_bytes)
|
||||
Reference in New Issue
Block a user