mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-07-27 23:37:45 +00:00
updated code structure
This commit is contained in:
@@ -21,6 +21,7 @@ from solders.transaction import VersionedTransaction
|
||||
from spl.token.instructions import get_associated_token_address
|
||||
|
||||
from config import *
|
||||
from trade import trade
|
||||
|
||||
# Here and later all the discriminators are precalculated. See learning-examples/discriminator.py
|
||||
EXPECTED_DISCRIMINATOR = struct.pack("<Q", 6966180631402821399)
|
||||
+3
-3
@@ -1,3 +1,3 @@
|
||||
RPC_ENDPOINT=...
|
||||
WSS_ENDPOINT=...
|
||||
PRIVATE_KEY=...
|
||||
SOLANA_NODE_RPC_ENDPOINT=...
|
||||
SOLANA_NODE_WSS_ENDPOINT=...
|
||||
SOLANA_PRIVATE_KEY=...
|
||||
Vendored
+5
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"python.languageServer": "Pylance",
|
||||
"python.analysis.typeCheckingMode": "basic",
|
||||
"python.analysis.diagnosticSeverityOverrides": {
|
||||
"reportMissingTypeStubs": "none",
|
||||
"reportUnknownMemberType": "warning",
|
||||
"reportUnknownParameterType": "warning"
|
||||
},
|
||||
|
||||
"ruff.lint.enable": true,
|
||||
"ruff.format.enable": true,
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Command-line interface for the pump.fun trading bot.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from typing import Any, Dict
|
||||
|
||||
import config
|
||||
from src.trading.trader import PumpTrader
|
||||
from src.utils.logger import get_logger, setup_file_logging
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""Parse command line arguments.
|
||||
|
||||
Returns:
|
||||
Parsed arguments
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="Trade tokens on pump.fun.")
|
||||
parser.add_argument(
|
||||
"--yolo", action="store_true", help="Run in YOLO mode (continuous trading)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--match",
|
||||
type=str,
|
||||
help="Only trade tokens with names or symbols matching this string",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bro", type=str, help="Only trade tokens created by this user address"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--marry", action="store_true", help="Only buy tokens, skip selling"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--amount",
|
||||
type=float,
|
||||
help=f"Amount of SOL to spend on each buy (default: {config.BUY_AMOUNT})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--buy-slippage",
|
||||
type=float,
|
||||
help=f"Buy slippage tolerance (default: {config.BUY_SLIPPAGE})",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--sell-slippage",
|
||||
type=float,
|
||||
help=f"Sell slippage tolerance (default: {config.SELL_SLIPPAGE})",
|
||||
)
|
||||
parser.add_argument("--rpc", type=str, help="Solana RPC endpoint")
|
||||
parser.add_argument("--wss", type=str, help="Solana WebSocket endpoint")
|
||||
|
||||
parser.add_argument("--wss", type=str, help="Solana WebSocket endpoint")
|
||||
parser.add_argument(
|
||||
"--key",
|
||||
type=str,
|
||||
help="Solana private key (better to use environment variable)",
|
||||
)
|
||||
parser.add_argument("--log", type=str, help="Log file path")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Main entry point for the CLI."""
|
||||
setup_file_logging("pump_trading.log")
|
||||
|
||||
args = parse_args()
|
||||
|
||||
# Get configuration values, preferring command line args over config.py
|
||||
rpc_endpoint: str | None = args.rpc or os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
||||
wss_endpoint: str | None = args.wss or os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
||||
private_key: str | None = args.key or os.environ.get("SOLANA_PRIVATE_KEY")
|
||||
|
||||
if not rpc_endpoint or rpc_endpoint.startswith(("http://", "https://")):
|
||||
logger.error("Invalid RPC endpoint. Must start with http:// or https://")
|
||||
sys.exit(1)
|
||||
|
||||
if not wss_endpoint or wss_endpoint.startswith(("ws://", "wss://")):
|
||||
logger.error("Invalid WebSocket endpoint. Must start with ws:// or wss://")
|
||||
sys.exit(1)
|
||||
|
||||
if not private_key or len(private_key) < 80:
|
||||
logger.error("Invalid private key. Key appears to be missing or too short")
|
||||
sys.exit(1)
|
||||
|
||||
buy_amount: float = args.amount if args.amount is not None else config.BUY_AMOUNT
|
||||
buy_slippage: float = (
|
||||
args.buy_slippage if args.buy_slippage is not None else config.BUY_SLIPPAGE
|
||||
)
|
||||
sell_slippage: float = (
|
||||
args.sell_slippage if args.sell_slippage is not None else config.SELL_SLIPPAGE
|
||||
)
|
||||
|
||||
trader: PumpTrader = PumpTrader(
|
||||
rpc_endpoint=rpc_endpoint, # type: ignore
|
||||
wss_endpoint=wss_endpoint, # type: ignore
|
||||
private_key=private_key,
|
||||
buy_amount=buy_amount,
|
||||
buy_slippage=buy_slippage,
|
||||
sell_slippage=sell_slippage,
|
||||
max_retries=config.MAX_RETRIES,
|
||||
)
|
||||
|
||||
try:
|
||||
await trader.start(
|
||||
match_string=args.match,
|
||||
bro_address=args.bro,
|
||||
marry_mode=args.marry,
|
||||
yolo_mode=args.yolo,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
logger.info("Trading stopped by user")
|
||||
except Exception as e:
|
||||
logger.error(f"Trading stopped due to error: {str(e)}")
|
||||
finally:
|
||||
try:
|
||||
await trader.solana_client.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -1,3 +1,9 @@
|
||||
"""
|
||||
Configuration for the pump.fun trading bot.
|
||||
"""
|
||||
|
||||
from typing import Final
|
||||
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
@@ -6,23 +12,33 @@ from solders.pubkey import Pubkey
|
||||
load_dotenv()
|
||||
|
||||
# System & pump.fun addresses
|
||||
PUMP_PROGRAM = Pubkey.from_string("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P")
|
||||
PUMP_GLOBAL = Pubkey.from_string("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf")
|
||||
PUMP_EVENT_AUTHORITY = Pubkey.from_string(
|
||||
PUMP_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
|
||||
)
|
||||
PUMP_GLOBAL: Final[Pubkey] = Pubkey.from_string(
|
||||
"4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf"
|
||||
)
|
||||
PUMP_EVENT_AUTHORITY: Final[Pubkey] = Pubkey.from_string(
|
||||
"Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1"
|
||||
)
|
||||
PUMP_FEE = Pubkey.from_string("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM")
|
||||
PUMP_LIQUIDITY_MIGRATOR = Pubkey.from_string(
|
||||
PUMP_FEE: Final[Pubkey] = Pubkey.from_string(
|
||||
"CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"
|
||||
)
|
||||
PUMP_LIQUIDITY_MIGRATOR: Final[Pubkey] = Pubkey.from_string(
|
||||
"39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg"
|
||||
)
|
||||
SYSTEM_PROGRAM = Pubkey.from_string("11111111111111111111111111111111")
|
||||
SYSTEM_TOKEN_PROGRAM = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM = Pubkey.from_string(
|
||||
SYSTEM_PROGRAM: Final[Pubkey] = Pubkey.from_string("11111111111111111111111111111111")
|
||||
SYSTEM_TOKEN_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
||||
)
|
||||
SYSTEM_ASSOCIATED_TOKEN_ACCOUNT_PROGRAM: Final[Pubkey] = Pubkey.from_string(
|
||||
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
|
||||
)
|
||||
SYSTEM_RENT = Pubkey.from_string("SysvarRent111111111111111111111111111111111")
|
||||
SOL = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
||||
LAMPORTS_PER_SOL = 1_000_000_000
|
||||
SYSTEM_RENT: Final[Pubkey] = Pubkey.from_string(
|
||||
"SysvarRent111111111111111111111111111111111"
|
||||
)
|
||||
SOL: Final[Pubkey] = Pubkey.from_string("So11111111111111111111111111111111111111112")
|
||||
LAMPORTS_PER_SOL: Final[int] = 1_000_000_000
|
||||
|
||||
# Trading parameters
|
||||
BUY_AMOUNT = 0.0001 # Amount of SOL to spend when buying
|
||||
@@ -33,11 +49,12 @@ ENABLE_DYNAMIC_PRIORITY_FEE = (
|
||||
)
|
||||
EXTRA_PRIORITY_FEE = 0.1 # 10% increase in dynamic priority fee
|
||||
|
||||
# Your nodes
|
||||
# You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes
|
||||
RPC_ENDPOINT = os.getenv("RPC_ENDPOINT")
|
||||
WSS_ENDPOINT = os.getenv("WSS_ENDPOINT")
|
||||
MAX_RPS = 25 # RPS of your node to avoid 429 errors
|
||||
TOKEN_DECIMALS: Final[int] = 6
|
||||
MAX_RETRIES: int = 5
|
||||
WAIT_TIME_AFTER_BUY: int = 20
|
||||
WAIT_TIME_BEFORE_NEW_TOKEN: int = 5
|
||||
WAIT_TIME_AFTER_CREATION: int = 15
|
||||
|
||||
# Private key
|
||||
PRIVATE_KEY = os.getenv("PRIVATE_KEY")
|
||||
# RPS of your node to avoid rate limit errors
|
||||
# You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes
|
||||
MAX_RPS = 25
|
||||
|
||||
+9
-4
@@ -1,6 +1,11 @@
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
target-version = ['py39']
|
||||
target-version = "py311"
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "UP", "N", "B", "A", "C4", "T10", "ARG", "PTH"]
|
||||
ignore = ["E501"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
@@ -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)
|
||||
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Event processing for pump.fun tokens.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import struct
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
from solders.transaction import VersionedTransaction
|
||||
|
||||
from src.trading.base import TokenInfo
|
||||
from src.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PumpEventProcessor:
|
||||
"""Processes events from pump.fun program."""
|
||||
|
||||
# Discriminator for create instruction
|
||||
CREATE_DISCRIMINATOR = 8576854823835016728
|
||||
|
||||
def __init__(self, pump_program: Pubkey):
|
||||
"""Initialize event processor.
|
||||
|
||||
Args:
|
||||
pump_program: Pump.fun program address
|
||||
"""
|
||||
self.pump_program = pump_program
|
||||
self._idl = self._load_idl()
|
||||
|
||||
def _load_idl(self) -> Dict[str, Any]:
|
||||
"""Load IDL from file.
|
||||
|
||||
Returns:
|
||||
IDL as dictionary
|
||||
"""
|
||||
try:
|
||||
with open("idl/pump_fun_idl.json", "r") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load IDL: {str(e)}")
|
||||
# Create a minimal IDL with just what we need
|
||||
return {
|
||||
"instructions": [
|
||||
{
|
||||
"name": "create",
|
||||
"args": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "symbol", "type": "string"},
|
||||
{"name": "uri", "type": "string"},
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def process_transaction(self, tx_data: str) -> Optional[TokenInfo]:
|
||||
"""Process a transaction and extract token info.
|
||||
|
||||
Args:
|
||||
tx_data: Base64 encoded transaction data
|
||||
|
||||
Returns:
|
||||
TokenInfo if a token creation is found, None otherwise
|
||||
"""
|
||||
try:
|
||||
tx_data_decoded = base64.b64decode(tx_data)
|
||||
transaction = VersionedTransaction.from_bytes(tx_data_decoded)
|
||||
|
||||
for ix in transaction.message.instructions:
|
||||
# Check if instruction is from pump.fun program
|
||||
program_id_index = ix.program_id_index
|
||||
if program_id_index >= len(transaction.message.account_keys):
|
||||
continue
|
||||
|
||||
program_id = transaction.message.account_keys[program_id_index]
|
||||
|
||||
if str(program_id) != str(self.pump_program):
|
||||
continue
|
||||
|
||||
ix_data = bytes(ix.data)
|
||||
|
||||
# Check if it's a create instruction
|
||||
if len(ix_data) < 8:
|
||||
continue
|
||||
|
||||
discriminator = struct.unpack("<Q", ix_data[:8])[0]
|
||||
if discriminator != self.CREATE_DISCRIMINATOR:
|
||||
continue
|
||||
|
||||
# Found a create instruction, decode it
|
||||
create_ix = next(
|
||||
(
|
||||
instr
|
||||
for instr in self._idl["instructions"]
|
||||
if instr["name"] == "create"
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not create_ix:
|
||||
continue
|
||||
|
||||
# Get account keys for this instruction
|
||||
account_keys = [
|
||||
transaction.message.account_keys[index] for index in ix.accounts
|
||||
]
|
||||
|
||||
# Decode instruction arguments
|
||||
decoded_args = self._decode_create_instruction(
|
||||
ix_data, create_ix, account_keys
|
||||
)
|
||||
|
||||
return TokenInfo(
|
||||
name=decoded_args["name"],
|
||||
symbol=decoded_args["symbol"],
|
||||
uri=decoded_args["uri"],
|
||||
mint=Pubkey.from_string(decoded_args["mint"]),
|
||||
bonding_curve=Pubkey.from_string(decoded_args["bondingCurve"]),
|
||||
associated_bonding_curve=Pubkey.from_string(
|
||||
decoded_args["associatedBondingCurve"]
|
||||
),
|
||||
user=Pubkey.from_string(decoded_args["user"]),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing transaction: {str(e)}")
|
||||
|
||||
return None
|
||||
|
||||
def _decode_create_instruction(
|
||||
self, ix_data: bytes, ix_def: Dict[str, Any], accounts: List[Pubkey]
|
||||
) -> Dict[str, Any]:
|
||||
"""Decode create instruction data.
|
||||
|
||||
Args:
|
||||
ix_data: Instruction data bytes
|
||||
ix_def: Instruction definition from IDL
|
||||
accounts: List of account pubkeys
|
||||
|
||||
Returns:
|
||||
Decoded instruction arguments
|
||||
"""
|
||||
args = {}
|
||||
offset = 8 # Skip 8-byte discriminator
|
||||
|
||||
for arg in ix_def["args"]:
|
||||
if arg["type"] == "string":
|
||||
length = struct.unpack_from("<I", ix_data, offset)[0]
|
||||
offset += 4
|
||||
value = ix_data[offset : offset + length].decode("utf-8")
|
||||
offset += length
|
||||
elif arg["type"] == "publicKey":
|
||||
value = base64.b64encode(ix_data[offset : offset + 32]).decode("utf-8")
|
||||
offset += 32
|
||||
else:
|
||||
logger.warning(f"Unsupported type: {arg['type']}")
|
||||
value = None
|
||||
|
||||
args[arg["name"]] = value
|
||||
|
||||
args["mint"] = str(accounts[0])
|
||||
args["bondingCurve"] = str(accounts[2])
|
||||
args["associatedBondingCurve"] = str(accounts[3])
|
||||
args["user"] = str(accounts[7])
|
||||
|
||||
return args
|
||||
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
WebSocket monitoring for pump.fun tokens.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from typing import Any, Awaitable, Callable, Dict, Optional
|
||||
|
||||
import websockets
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
from src.monitoring.events import PumpEventProcessor
|
||||
from src.trading.base import TokenInfo
|
||||
from src.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PumpTokenListener:
|
||||
"""WebSocket listener for pump.fun token creation events."""
|
||||
|
||||
def __init__(self, wss_endpoint: str, pump_program: Pubkey):
|
||||
"""Initialize token listener.
|
||||
|
||||
Args:
|
||||
wss_endpoint: WebSocket endpoint URL
|
||||
pump_program: Pump.fun program address
|
||||
"""
|
||||
self.wss_endpoint = wss_endpoint
|
||||
self.pump_program = pump_program
|
||||
self.event_processor = PumpEventProcessor(pump_program)
|
||||
self.ping_interval = 20 # seconds
|
||||
|
||||
async def listen_for_tokens(
|
||||
self,
|
||||
token_callback: Callable[[TokenInfo], Awaitable[None]],
|
||||
match_string: str | None = None,
|
||||
creator_address: str | None = None,
|
||||
) -> None:
|
||||
"""Listen for new token creations.
|
||||
|
||||
Args:
|
||||
token_callback: Callback function for new tokens
|
||||
match_string: Optional string to match in token name/symbol
|
||||
creator_address: Optional creator address to filter by
|
||||
"""
|
||||
while True:
|
||||
try:
|
||||
async with websockets.connect(self.wss_endpoint) as websocket:
|
||||
await self._subscribe_to_program(websocket)
|
||||
ping_task = asyncio.create_task(self._ping_loop(websocket))
|
||||
|
||||
try:
|
||||
while True:
|
||||
token_info = await self._wait_for_token_creation(websocket)
|
||||
if not token_info:
|
||||
continue
|
||||
|
||||
logger.info(
|
||||
f"New token detected: {token_info.name} ({token_info.symbol})"
|
||||
)
|
||||
|
||||
# Apply filters
|
||||
if match_string and not (
|
||||
match_string.lower() in token_info.name.lower()
|
||||
or match_string.lower() in token_info.symbol.lower()
|
||||
):
|
||||
logger.info(
|
||||
f"Token does not match filter '{match_string}'. Skipping..."
|
||||
)
|
||||
continue
|
||||
|
||||
if (
|
||||
creator_address
|
||||
and str(token_info.user) != creator_address
|
||||
):
|
||||
logger.info(
|
||||
f"Token not created by {creator_address}. Skipping..."
|
||||
)
|
||||
continue
|
||||
|
||||
await token_callback(token_info)
|
||||
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.warning("WebSocket connection closed. Reconnecting...")
|
||||
ping_task.cancel()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket connection error: {str(e)}")
|
||||
logger.info("Reconnecting in 5 seconds...")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
async def _subscribe_to_program(self, websocket) -> None:
|
||||
"""Subscribe to blocks mentioning the pump.fun program.
|
||||
|
||||
Args:
|
||||
websocket: Active WebSocket connection
|
||||
"""
|
||||
subscription_message = json.dumps(
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "blockSubscribe",
|
||||
"params": [
|
||||
{"mentionsAccountOrProgram": str(self.pump_program)},
|
||||
{
|
||||
"commitment": "confirmed",
|
||||
"encoding": "base64",
|
||||
"showRewards": False,
|
||||
"transactionDetails": "full",
|
||||
"maxSupportedTransactionVersion": 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
await websocket.send(subscription_message)
|
||||
logger.info(f"Subscribed to blocks mentioning program: {self.pump_program}")
|
||||
|
||||
async def _ping_loop(self, websocket) -> None:
|
||||
"""Keep connection alive with pings.
|
||||
|
||||
Args:
|
||||
websocket: Active WebSocket connection
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(self.ping_interval)
|
||||
await websocket.ping()
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Ping error: {str(e)}")
|
||||
|
||||
async def _wait_for_token_creation(self, websocket) -> Optional[TokenInfo]:
|
||||
"""Wait for token creation event.
|
||||
|
||||
Args:
|
||||
websocket: Active WebSocket connection
|
||||
|
||||
Returns:
|
||||
TokenInfo if a token creation is found, None otherwise
|
||||
"""
|
||||
try:
|
||||
response = await asyncio.wait_for(websocket.recv(), timeout=30)
|
||||
data = json.loads(response)
|
||||
|
||||
if "method" not in data or data["method"] != "blockNotification":
|
||||
return None
|
||||
|
||||
if "params" not in data or "result" not in data["params"]:
|
||||
return None
|
||||
|
||||
block_data = data["params"]["result"]
|
||||
if "value" not in block_data or "block" not in block_data["value"]:
|
||||
return None
|
||||
|
||||
block = block_data["value"]["block"]
|
||||
if "transactions" not in block:
|
||||
return None
|
||||
|
||||
for tx in block["transactions"]:
|
||||
if not isinstance(tx, dict) or "transaction" not in tx:
|
||||
continue
|
||||
|
||||
token_info = self.event_processor.process_transaction(
|
||||
tx["transaction"][0]
|
||||
)
|
||||
if token_info:
|
||||
return token_info
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
logger.debug("No data received for 30 seconds")
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
logger.warning("WebSocket connection closed")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing WebSocket message: {str(e)}")
|
||||
|
||||
return None
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Logging utilities for the pump.fun trading bot.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from typing import Dict
|
||||
|
||||
# Global dict to store loggers
|
||||
_loggers: Dict[str, logging.Logger] = {}
|
||||
|
||||
|
||||
def get_logger(name: str, level: int = logging.INFO) -> logging.Logger:
|
||||
"""Get or create a logger with the given name.
|
||||
|
||||
Args:
|
||||
name: Logger name, typically __name__
|
||||
level: Logging level
|
||||
|
||||
Returns:
|
||||
Configured logger
|
||||
"""
|
||||
global _loggers
|
||||
|
||||
if name in _loggers:
|
||||
return _loggers[name]
|
||||
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(level)
|
||||
|
||||
if not logger.handlers:
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setFormatter(formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
_loggers[name] = logger
|
||||
return logger
|
||||
|
||||
|
||||
def setup_file_logging(
|
||||
filename: str = "pump_trading.log", level: int = logging.INFO
|
||||
) -> None:
|
||||
"""Set up file logging for all loggers.
|
||||
|
||||
Args:
|
||||
filename: Log file path
|
||||
level: Logging level for file handler
|
||||
"""
|
||||
root_logger = logging.getLogger()
|
||||
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
file_handler = logging.FileHandler(filename)
|
||||
file_handler.setLevel(level)
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
root_logger.addHandler(file_handler)
|
||||
Reference in New Issue
Block a user