updated code structure

This commit is contained in:
smypmsa
2025-03-05 16:44:55 +00:00
parent 8bf3700187
commit 1567c4df7d
23 changed files with 1764 additions and 25 deletions
View File
+162
View File
@@ -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
+136
View File
@@ -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
+51
View File
@@ -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"
)
+55
View File
@@ -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)