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
+83
View File
@@ -0,0 +1,83 @@
"""
Base interfaces for trading operations.
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Tuple
from solders.pubkey import Pubkey
from solders.signature import Signature
@dataclass
class TokenInfo:
"""Token information."""
name: str
symbol: str
uri: str
mint: Pubkey
bonding_curve: Pubkey
associated_bonding_curve: Pubkey
user: Pubkey
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> "TokenInfo":
"""Create TokenInfo from dictionary.
Args:
data: Dictionary with token data
Returns:
TokenInfo instance
"""
return cls(
name=data["name"],
symbol=data["symbol"],
uri=data["uri"],
mint=Pubkey.from_string(data["mint"]),
bonding_curve=Pubkey.from_string(data["bondingCurve"]),
associated_bonding_curve=Pubkey.from_string(data["associatedBondingCurve"]),
user=Pubkey.from_string(data["user"]),
)
def to_dict(self) -> Dict[str, str]:
"""Convert to dictionary.
Returns:
Dictionary representation
"""
return {
"name": self.name,
"symbol": self.symbol,
"uri": self.uri,
"mint": str(self.mint),
"bondingCurve": str(self.bonding_curve),
"associatedBondingCurve": str(self.associated_bonding_curve),
"user": str(self.user),
}
@dataclass
class TradeResult:
"""Result of a trading operation."""
success: bool
tx_signature: Optional[str] = None
error_message: Optional[str] = None
amount: Optional[float] = None
price: Optional[float] = None
class Trader(ABC):
"""Base interface for trading operations."""
@abstractmethod
async def execute(self, *args, **kwargs) -> TradeResult:
"""Execute trading operation.
Returns:
TradeResult with operation outcome
"""
pass
+253
View File
@@ -0,0 +1,253 @@
"""
Buy operations for pump.fun tokens.
"""
import asyncio
import struct
from typing import List, Optional
import spl.token.instructions as spl_token
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 AccountMeta, Instruction
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from solders.system_program import TransferParams, transfer
from spl.token.instructions import get_associated_token_address
from src.core.client import SolanaClient
from src.core.curve import BondingCurveManager
from src.core.pubkeys import (
LAMPORTS_PER_SOL,
TOKEN_DECIMALS,
PumpAddresses,
SystemAddresses,
)
from src.core.wallet import Wallet
from src.trading.base import TokenInfo, Trader, TradeResult
from src.utils.logger import get_logger
logger = get_logger(__name__)
class TokenBuyer(Trader):
"""Handles buying tokens on pump.fun."""
def __init__(
self,
client: SolanaClient,
wallet: Wallet,
curve_manager: BondingCurveManager,
amount: float,
slippage: float = 0.01,
max_retries: int = 5,
):
"""Initialize token buyer.
Args:
client: Solana client for RPC calls
wallet: Wallet for signing transactions
curve_manager: Bonding curve manager
amount: Amount of SOL to spend
slippage: Slippage tolerance (0.01 = 1%)
max_retries: Maximum number of retry attempts
"""
self.client = client
self.wallet = wallet
self.curve_manager = curve_manager
self.amount = amount
self.slippage = slippage
self.max_retries = max_retries
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult:
"""Execute buy operation.
Args:
token_info: Token information
Returns:
TradeResult with buy outcome
"""
try:
# Extract token info
mint = token_info.mint
bonding_curve = token_info.bonding_curve
associated_bonding_curve = token_info.associated_bonding_curve
# Convert amount to lamports
amount_lamports = int(self.amount * LAMPORTS_PER_SOL)
# Fetch token price
curve_state = await self.curve_manager.get_curve_state(bonding_curve)
token_price_sol = curve_state.calculate_price()
token_amount = self.amount / token_price_sol
# Calculate maximum SOL to spend with slippage
max_amount_lamports = int(amount_lamports * (1 + self.slippage))
logger.info(
f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token"
)
logger.info(
f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)"
)
associated_token_account = self.wallet.get_associated_token_address(mint)
await self._ensure_associated_token_account(mint, associated_token_account)
tx_signature = await self._send_buy_transaction(
mint,
bonding_curve,
associated_bonding_curve,
associated_token_account,
token_amount,
max_amount_lamports,
)
success = await self.client.confirm_transaction(tx_signature)
if success:
logger.info(f"Buy transaction confirmed: {tx_signature}")
return TradeResult(
success=True,
tx_signature=tx_signature,
amount=token_amount,
price=token_price_sol,
)
else:
return TradeResult(
success=False,
error_message=f"Transaction failed to confirm: {tx_signature}",
)
except Exception as e:
logger.error(f"Buy operation failed: {str(e)}")
return TradeResult(success=False, error_message=str(e))
async def _ensure_associated_token_account(
self, mint: Pubkey, associated_token_account: Pubkey
) -> None:
"""Ensure associated token account exists.
Args:
mint: Token mint
associated_token_account: Associated token account address
"""
try:
solana_client = await self.client.get_client()
account_info = await solana_client.get_account_info(
associated_token_account
)
if account_info.value is None:
logger.info(f"Creating associated token account for {mint}...")
create_ata_ix = spl_token.create_associated_token_account(
payer=self.wallet.pubkey, owner=self.wallet.pubkey, mint=mint
)
create_ata_tx = Transaction()
create_ata_tx.add(create_ata_ix)
blockhash = await self.client.get_latest_blockhash()
create_ata_tx.recent_blockhash = blockhash
tx_sig = await self.client.send_transaction(
create_ata_tx, self.wallet.keypair
)
await self.client.confirm_transaction(tx_sig)
logger.info(
f"Associated token account created: {associated_token_account}"
)
else:
logger.info(
f"Associated token account already exists: {associated_token_account}"
)
except Exception as e:
logger.error(f"Error creating associated token account: {str(e)}")
raise
async def _send_buy_transaction(
self,
mint: Pubkey,
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
associated_token_account: Pubkey,
token_amount: float,
max_amount_lamports: int,
) -> str:
"""Send buy transaction.
Args:
mint: Token mint
bonding_curve: Bonding curve address
associated_bonding_curve: Associated bonding curve address
associated_token_account: User's token account
token_amount: Amount of tokens to buy
max_amount_lamports: Maximum SOL to spend in lamports
Returns:
Transaction signature
Raises:
Exception: If transaction fails after all retries
"""
accounts = [
AccountMeta(
pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False
),
AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True),
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta(
pubkey=associated_bonding_curve, is_signer=False, is_writable=True
),
AccountMeta(
pubkey=associated_token_account, is_signer=False, is_writable=True
),
AccountMeta(pubkey=self.wallet.pubkey, is_signer=True, is_writable=True),
AccountMeta(
pubkey=SystemAddresses.PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=SystemAddresses.RENT, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PumpAddresses.EVENT_AUTHORITY, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PumpAddresses.PROGRAM, is_signer=False, is_writable=False
),
]
# Prepare buy instruction data
# Discriminator for buy instruction
discriminator = struct.pack("<Q", 16927863322537952870)
token_amount_raw = int(token_amount * 10**TOKEN_DECIMALS)
data = (
discriminator
+ struct.pack("<Q", token_amount_raw)
+ struct.pack("<Q", max_amount_lamports)
)
buy_ix = Instruction(PumpAddresses.PROGRAM, data, accounts)
transaction = Transaction()
transaction.add(buy_ix)
try:
return await self.client.send_transaction(
transaction,
self.wallet.keypair,
skip_preflight=True,
max_retries=self.max_retries,
)
except Exception as e:
logger.error(f"Buy transaction failed: {str(e)}")
raise
+214
View File
@@ -0,0 +1,214 @@
"""
Sell operations for pump.fun tokens.
"""
import asyncio
import struct
from typing import Optional
from solana.transaction import Transaction
from solders.instruction import AccountMeta, Instruction
from solders.pubkey import Pubkey
from src.core.client import SolanaClient
from src.core.curve import BondingCurveManager
from src.core.pubkeys import (
LAMPORTS_PER_SOL,
TOKEN_DECIMALS,
PumpAddresses,
SystemAddresses,
)
from src.core.wallet import Wallet
from src.trading.base import TokenInfo, Trader, TradeResult
from src.utils.logger import get_logger
logger = get_logger(__name__)
class TokenSeller(Trader):
"""Handles selling tokens on pump.fun."""
def __init__(
self,
client: SolanaClient,
wallet: Wallet,
curve_manager: BondingCurveManager,
slippage: float = 0.25,
max_retries: int = 5,
):
"""Initialize token seller.
Args:
client: Solana client for RPC calls
wallet: Wallet for signing transactions
curve_manager: Bonding curve manager
slippage: Slippage tolerance (0.25 = 25%)
max_retries: Maximum number of retry attempts
"""
self.client = client
self.wallet = wallet
self.curve_manager = curve_manager
self.slippage = slippage
self.max_retries = max_retries
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult:
"""Execute sell operation.
Args:
token_info: Token information
Returns:
TradeResult with sell outcome
"""
try:
# Extract token info
mint = token_info.mint
bonding_curve = token_info.bonding_curve
associated_bonding_curve = token_info.associated_bonding_curve
# Get associated token account
associated_token_account = self.wallet.get_associated_token_address(mint)
# Get token balance
token_balance = await self.client.get_token_account_balance(
associated_token_account
)
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
logger.info(f"Token balance: {token_balance_decimal}")
if token_balance == 0:
logger.info("No tokens to sell.")
return TradeResult(success=False, error_message="No tokens to sell")
# Fetch token price
curve_state = await self.curve_manager.get_curve_state(bonding_curve)
token_price_sol = curve_state.calculate_price()
logger.info(f"Price per Token: {token_price_sol:.8f} SOL")
# Calculate minimum SOL output with slippage
amount = token_balance
expected_sol_output = float(token_balance_decimal) * float(token_price_sol)
slippage_factor = 1 - self.slippage
min_sol_output = int(
(expected_sol_output * slippage_factor) * LAMPORTS_PER_SOL
)
logger.info(f"Selling {token_balance_decimal} tokens")
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"
)
tx_signature = await self._send_sell_transaction(
mint,
bonding_curve,
associated_bonding_curve,
associated_token_account,
amount,
min_sol_output,
)
success = await self.client.confirm_transaction(tx_signature)
if success:
logger.info(f"Sell transaction confirmed: {tx_signature}")
return TradeResult(
success=True,
tx_signature=tx_signature,
amount=token_balance_decimal,
price=token_price_sol,
)
else:
return TradeResult(
success=False,
error_message=f"Transaction failed to confirm: {tx_signature}",
)
except Exception as e:
logger.error(f"Sell operation failed: {str(e)}")
return TradeResult(success=False, error_message=str(e))
async def _send_sell_transaction(
self,
mint: Pubkey,
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
associated_token_account: Pubkey,
token_amount: int,
min_sol_output: int,
) -> str:
"""Send sell transaction.
Args:
mint: Token mint
bonding_curve: Bonding curve address
associated_bonding_curve: Associated bonding curve address
associated_token_account: User's token account
token_amount: Amount of tokens to sell in raw units
min_sol_output: Minimum SOL to receive in lamports
Returns:
Transaction signature
Raises:
Exception: If transaction fails after all retries
"""
# Prepare sell instruction accounts
accounts = [
AccountMeta(
pubkey=PumpAddresses.GLOBAL, is_signer=False, is_writable=False
),
AccountMeta(pubkey=PumpAddresses.FEE, is_signer=False, is_writable=True),
AccountMeta(pubkey=mint, is_signer=False, is_writable=False),
AccountMeta(pubkey=bonding_curve, is_signer=False, is_writable=True),
AccountMeta(
pubkey=associated_bonding_curve, is_signer=False, is_writable=True
),
AccountMeta(
pubkey=associated_token_account, is_signer=False, is_writable=True
),
AccountMeta(pubkey=self.wallet.pubkey, is_signer=True, is_writable=True),
AccountMeta(
pubkey=SystemAddresses.PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=SystemAddresses.ASSOCIATED_TOKEN_PROGRAM,
is_signer=False,
is_writable=False,
),
AccountMeta(
pubkey=SystemAddresses.TOKEN_PROGRAM, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PumpAddresses.EVENT_AUTHORITY, is_signer=False, is_writable=False
),
AccountMeta(
pubkey=PumpAddresses.PROGRAM, is_signer=False, is_writable=False
),
]
# Prepare sell instruction data
# Discriminator for sell instruction
discriminator = struct.pack("<Q", 12502976635542562355)
data = (
discriminator
+ struct.pack("<Q", token_amount)
+ struct.pack("<Q", min_sol_output)
)
sell_ix = Instruction(PumpAddresses.PROGRAM, data, accounts)
transaction = Transaction()
transaction.add(sell_ix)
try:
return await self.client.send_transaction(
transaction,
self.wallet.keypair,
skip_preflight=True,
max_retries=self.max_retries,
)
except Exception as e:
logger.error(f"Sell transaction failed: {str(e)}")
raise
+215
View File
@@ -0,0 +1,215 @@
"""
Main trading coordinator for pump.fun tokens.
"""
import asyncio
import json
import os
from datetime import datetime
from typing import Any, Dict, List, Optional
from solders.pubkey import Pubkey
from src.core.client import SolanaClient
from src.core.curve import BondingCurveManager
from src.core.pubkeys import PumpAddresses
from src.core.wallet import Wallet
from src.monitoring.listener import PumpTokenListener
from src.trading.base import TokenInfo, TradeResult
from src.trading.buyer import TokenBuyer
from src.trading.seller import TokenSeller
from src.utils.logger import get_logger
logger = get_logger(__name__)
class PumpTrader:
"""Coordinates trading operations for pump.fun tokens."""
def __init__(
self,
rpc_endpoint: str,
wss_endpoint: str,
private_key: str,
buy_amount: float,
buy_slippage: float,
sell_slippage: float,
max_retries: int = 5,
):
"""Initialize the pump trader.
Args:
rpc_endpoint: RPC endpoint URL
wss_endpoint: WebSocket endpoint URL
private_key: Wallet private key
buy_amount: Amount of SOL to spend on buys
buy_slippage: Slippage tolerance for buys
sell_slippage: Slippage tolerance for sells
max_retries: Maximum number of retry attempts
"""
self.solana_client = SolanaClient(rpc_endpoint)
self.wallet = Wallet(private_key)
self.curve_manager = BondingCurveManager(self.solana_client)
self.buyer = TokenBuyer(
self.solana_client,
self.wallet,
self.curve_manager,
buy_amount,
buy_slippage,
max_retries,
)
self.seller = TokenSeller(
self.solana_client,
self.wallet,
self.curve_manager,
sell_slippage,
max_retries,
)
self.token_listener = PumpTokenListener(wss_endpoint, PumpAddresses.PROGRAM)
self.buy_amount = buy_amount
self.buy_slippage = buy_slippage
self.sell_slippage = sell_slippage
self.max_retries = max_retries
async def start(
self,
match_string: str | None = None,
bro_address: str | None = None,
marry_mode: bool = False,
yolo_mode: bool = False,
) -> None:
"""Start the trading bot.
Args:
match_string: Optional string to match in token name/symbol
bro_address: Optional creator address to filter by
marry_mode: If True, only buy tokens and skip selling
yolo_mode: If True, trade continuously
"""
logger.info("Starting pump.fun trader")
logger.info(f"Match filter: {match_string if match_string else 'None'}")
logger.info(f"Creator filter: {bro_address if bro_address else 'None'}")
logger.info(f"Marry mode: {marry_mode}")
logger.info(f"YOLO mode: {yolo_mode}")
try:
await self.token_listener.listen_for_tokens(
lambda token: self._handle_new_token(token, marry_mode, yolo_mode),
match_string,
bro_address,
)
except Exception as e:
logger.error(f"Trading stopped due to error: {str(e)}")
await self.solana_client.close()
async def _handle_new_token(
self, token_info: TokenInfo, marry_mode: bool, yolo_mode: bool
) -> None:
"""Handle a new token creation event.
Args:
token_info: Token information
marry_mode: If True, only buy tokens and skip selling
yolo_mode: If True, continue trading after this token
"""
try:
await self._save_token_info(token_info)
logger.info("Waiting for 15 seconds for the bonding curve to stabilize...")
await asyncio.sleep(15)
try:
token_price = await self.curve_manager.calculate_price(
token_info.bonding_curve
)
logger.info(f"Token price: {token_price:.10f} SOL")
except Exception as e:
logger.error(f"Failed to get token price: {str(e)}")
token_price = 0
logger.info(
f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol}..."
)
buy_result = await self.buyer.execute(token_info)
if buy_result.success:
logger.info(f"Successfully bought {token_info.symbol}")
self._log_trade("buy", token_info, token_price, buy_result.tx_signature)
else:
logger.error(
f"Failed to buy {token_info.symbol}: {buy_result.error_message}"
)
# Sell token if not in marry mode
if not marry_mode and buy_result.success:
logger.info("Waiting for 20 seconds before selling...")
await asyncio.sleep(20)
logger.info(f"Selling {token_info.symbol}...")
sell_result = await self.seller.execute(token_info)
if sell_result.success:
logger.info(f"Successfully sold {token_info.symbol}")
self._log_trade(
"sell", token_info, token_price, sell_result.tx_signature
)
else:
logger.error(
f"Failed to sell {token_info.symbol}: {sell_result.error_message}"
)
elif marry_mode:
logger.info("Marry mode enabled. Skipping sell operation.")
# Wait before looking for the next token
if yolo_mode:
logger.info(
"YOLO mode enabled. Waiting 5 seconds before looking for next token..."
)
await asyncio.sleep(5)
except Exception as e:
logger.error(f"Error handling token {token_info.symbol}: {str(e)}")
async def _save_token_info(self, token_info: TokenInfo) -> None:
"""Save token information to a file.
Args:
token_info: Token information
"""
os.makedirs("trades", exist_ok=True)
file_name = os.path.join("trades", f"{token_info.mint}.txt")
with open(file_name, "w") as file:
file.write(json.dumps(token_info.to_dict(), indent=2))
logger.info(f"Token information saved to {file_name}")
def _log_trade(
self, action: str, token_info: TokenInfo, price: float, tx_hash: str | None
) -> None:
"""Log trade information.
Args:
action: Trade action (buy/sell)
token_info: Token information
price: Token price in SOL
tx_hash: Transaction hash
"""
os.makedirs("trades", exist_ok=True)
log_entry = {
"timestamp": datetime.utcnow().isoformat(),
"action": action,
"token_address": str(token_info.mint),
"symbol": token_info.symbol,
"price": price,
"tx_hash": tx_hash,
}
with open("trades/trades.log", "a") as log_file:
log_file.write(json.dumps(log_entry) + "\n")