fixed and buy transactions

This commit is contained in:
smypmsa
2025-03-06 16:14:17 +00:00
parent a7b2c135f9
commit fce547ec9e
8 changed files with 133 additions and 67 deletions
+4 -1
View File
@@ -1,3 +1,5 @@
trades/*
.pylintrc
.ruff_cache
@@ -162,4 +164,5 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
#.idea/
trades/trades.log
+5 -18
View File
@@ -52,16 +52,7 @@ def parse_args() -> argparse.Namespace:
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()
@@ -73,23 +64,19 @@ async def main() -> None:
# Get configuration values, preferring command line args over config.py
rpc_endpoint: str | None = (
args.rpc
or os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
or config.PUBLIC_RPC_ENDPOINT
os.environ.get("SOLANA_NODE_RPC_ENDPOINT") or config.PUBLIC_RPC_ENDPOINT
)
wss_endpoint: str | None = (
args.wss
or os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
or config.PUBLIC_WSS_ENDPOINT
os.environ.get("SOLANA_NODE_WSS_ENDPOINT") or config.PUBLIC_WSS_ENDPOINT
)
private_key: str | None = args.key or os.environ.get("SOLANA_PRIVATE_KEY")
private_key: str | None = os.environ.get("SOLANA_PRIVATE_KEY")
# Validate configuration values
if not rpc_endpoint or rpc_endpoint.startswith(("http://", "https://")):
if not rpc_endpoint or not 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://")):
if not wss_endpoint or not wss_endpoint.startswith(("ws://", "wss://")):
logger.error("Invalid WebSocket endpoint. Must start with ws:// or wss://")
sys.exit(1)
+7 -7
View File
@@ -3,17 +3,17 @@ Configuration for the pump.fun trading bot.
"""
# Trading parameters
BUY_AMOUNT = 0.0001 # Amount of SOL to spend when buying
BUY_SLIPPAGE = 0.2 # 20% slippage tolerance for buying
SELL_SLIPPAGE = 0.2 # 20% slippage tolerance for selling
BUY_AMOUNT = 0.000001 # Amount of SOL to spend when buying
BUY_SLIPPAGE = 0.4 # 40% slippage tolerance for buying
SELL_SLIPPAGE = 0.4 # 40% slippage tolerance for selling
ENABLE_DYNAMIC_PRIORITY_FEE = True # TODO: not implemented. getRecentPriorityFee is used to get current priority fee
EXTRA_PRIORITY_FEE = 0.1 # TODO: not implemented. 10% increase in dynamic priority fee
# Retries and timeouts
MAX_RETRIES: int = 5
WAIT_TIME_AFTER_BUY: int = 5
WAIT_TIME_BEFORE_NEW_TOKEN: int = 5
WAIT_TIME_AFTER_CREATION: int = 5
MAX_RETRIES: int = 2
WAIT_TIME_AFTER_BUY: int = 15
WAIT_TIME_BEFORE_NEW_TOKEN: int = 120
WAIT_TIME_AFTER_CREATION: int = 15
# Node provier configuration
# You can also get a trader node https://docs.chainstack.com/docs/solana-trader-nodes
+3 -11
View File
@@ -8,6 +8,7 @@ from typing import Any, Dict
from solana.rpc.async_api import AsyncClient
from solana.rpc.commitment import Confirmed
from solana.rpc.types import TxOpts
from solders.hash import Hash
from solders.keypair import Keypair
from solders.pubkey import Pubkey
from solders.transaction import Transaction
@@ -78,7 +79,7 @@ class SolanaClient:
return int(response.value.amount)
return 0
async def get_latest_blockhash(self) -> str:
async def get_latest_blockhash(self) -> Hash:
"""Get the latest blockhash.
Returns:
@@ -91,7 +92,6 @@ class SolanaClient:
async def send_transaction(
self,
transaction: Transaction,
signer: Keypair,
skip_preflight: bool = True,
max_retries: int = 3,
) -> str:
@@ -99,7 +99,6 @@ class SolanaClient:
Args:
transaction: Prepared transaction
signer: Transaction signer
skip_preflight: Whether to skip preflight checks
max_retries: Maximum number of sending attempts
@@ -111,20 +110,13 @@ class SolanaClient:
"""
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
)
response = await client.send_transaction(transaction, opts=tx_opts)
return response.value
except Exception as e:
+8 -1
View File
@@ -125,7 +125,14 @@ class PumpTokenListener:
try:
while True:
await asyncio.sleep(self.ping_interval)
await websocket.ping()
try:
pong_waiter = await websocket.ping()
await asyncio.wait_for(pong_waiter, timeout=10)
except asyncio.TimeoutError:
logger.warning("Ping timeout - server not responding")
# Force reconnection
await websocket.close()
return
except asyncio.CancelledError:
pass
except Exception as e:
+20 -17
View File
@@ -4,18 +4,20 @@ Buy operations for pump.fun tokens.
import asyncio
import struct
from typing import List, Optional
from typing import Final, 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 solders.hash import Hash
from solders.instruction import AccountMeta, Instruction
from solders.keypair import Keypair
from solders.message import Message
from solders.pubkey import Pubkey
from solders.system_program import TransferParams, transfer
from solders.transaction import Transaction
from spl.token.instructions import get_associated_token_address
from spl.token.instructions import create_associated_token_account
from src.core.client import SolanaClient
from src.core.curve import BondingCurveManager
@@ -31,6 +33,9 @@ from src.utils.logger import get_logger
logger = get_logger(__name__)
# Discriminator for the buy instruction
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 16927863322537952870)
class TokenBuyer(Trader):
"""Handles buying tokens on pump.fun."""
@@ -145,19 +150,18 @@ class TokenBuyer(Trader):
if account_info.value is None:
logger.info(f"Creating associated token account for {mint}...")
create_ata_ix = spl_token.create_associated_token_account(
create_ata_ix = 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
recent_blockhash: Hash = await self.client.get_latest_blockhash()
create_ata_msg = Message([create_ata_ix], self.wallet.keypair.pubkey())
create_ata_tx = Transaction(
[self.wallet.keypair], create_ata_msg, recent_blockhash
)
tx_sig = await self.client.send_transaction(create_ata_tx)
await self.client.confirm_transaction(tx_sig)
logger.info(
f"Associated token account created: {associated_token_account}"
@@ -228,23 +232,22 @@ class TokenBuyer(Trader):
]
# Prepare buy instruction data
# Discriminator for buy instruction
discriminator = struct.pack("<Q", 16927863322537952870)
token_amount_raw = int(token_amount * 10**TOKEN_DECIMALS)
data = (
discriminator
EXPECTED_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)
# Prepare buy transaction data
recent_blockhash: Hash = await self.client.get_latest_blockhash()
buy_message = Message([buy_ix], self.wallet.keypair.pubkey())
buy_tx = Transaction([self.wallet.keypair], buy_message, recent_blockhash)
try:
return await self.client.send_transaction(
transaction,
self.wallet.keypair,
buy_tx,
skip_preflight=True,
max_retries=self.max_retries,
)
+12 -8
View File
@@ -4,9 +4,11 @@ Sell operations for pump.fun tokens.
import asyncio
import struct
from typing import Optional
from typing import Final, Optional
from solders.hash import Hash
from solders.instruction import AccountMeta, Instruction
from solders.message import Message
from solders.pubkey import Pubkey
from solders.transaction import Transaction
@@ -24,6 +26,9 @@ from src.utils.logger import get_logger
logger = get_logger(__name__)
# Discriminator for the sell instruction
EXPECTED_DISCRIMINATOR: Final[bytes] = struct.pack("<Q", 12502976635542562355)
class TokenSeller(Trader):
"""Handles selling tokens on pump.fun."""
@@ -190,22 +195,21 @@ class TokenSeller(Trader):
]
# Prepare sell instruction data
# Discriminator for sell instruction
discriminator = struct.pack("<Q", 12502976635542562355)
data = (
discriminator
EXPECTED_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)
# Prepare sell transaction data
recent_blockhash: Hash = await self.client.get_latest_blockhash()
sell_message = Message([sell_ix], self.wallet.keypair.pubkey())
sell_tx = Transaction([self.wallet.keypair], sell_message, recent_blockhash)
try:
return await self.client.send_transaction(
transaction,
self.wallet.keypair,
sell_tx,
skip_preflight=True,
max_retries=self.max_retries,
)
+74 -4
View File
@@ -1,11 +1,13 @@
"""
Main trading coordinator for pump.fun tokens.
Refactored PumpTrader to only process fresh tokens from WebSocket.
"""
import asyncio
import json
import os
from datetime import datetime
from typing import Dict, Optional, Set
import config
from src.core.client import SolanaClient
@@ -22,7 +24,7 @@ logger = get_logger(__name__)
class PumpTrader:
"""Coordinates trading operations for pump.fun tokens."""
"""Coordinates trading operations for pump.fun tokens with focus on freshness."""
def __init__(
self,
@@ -72,6 +74,13 @@ class PumpTrader:
self.buy_slippage = buy_slippage
self.sell_slippage = sell_slippage
self.max_retries = max_retries
self.max_token_age = max_token_age
# Token processing state
self.token_queue = asyncio.Queue()
self.processing = False
self.processed_tokens: Set[str] = set()
self.token_timestamps: Dict[str, float] = {}
async def start(
self,
@@ -93,19 +102,73 @@ class PumpTrader:
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}")
logger.info(f"Max token age: {self.max_token_age} seconds")
# Start processor task
processor_task = asyncio.create_task(
self._process_token_queue(marry_mode, yolo_mode)
)
try:
await self.token_listener.listen_for_tokens(
lambda token: self._handle_new_token(token, marry_mode, yolo_mode),
lambda token: self._queue_token(token),
match_string,
bro_address,
)
except Exception as e:
logger.error(f"Trading stopped due to error: {str(e)}")
processor_task.cancel()
await self.solana_client.close()
async def _handle_new_token(
async def _queue_token(self, token_info: TokenInfo) -> None:
"""Queue a token for processing if not already processed."""
token_key = str(token_info.mint)
if token_key in self.processed_tokens:
logger.debug(f"Token {token_info.symbol} already processed. Skipping...")
return
# Record timestamp when token was discovered
self.token_timestamps[token_key] = asyncio.get_event_loop().time()
# Add to queue
await self.token_queue.put(token_info)
logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint})")
async def _process_token_queue(self, marry_mode: bool, yolo_mode: bool) -> None:
"""Continuously process tokens from the queue, only if they're fresh."""
while True:
# Get next token
token_info = await self.token_queue.get()
token_key = str(token_info.mint)
# Check if token is still fresh
current_time = asyncio.get_event_loop().time()
token_age = current_time - self.token_timestamps.get(
token_key, current_time
)
if token_age > self.max_token_age:
logger.info(
f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)"
)
self.token_queue.task_done()
continue
# Mark as processing
self.processed_tokens.add(token_key)
# Process token
logger.info(
f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)"
)
await self._handle_token(token_info, marry_mode, yolo_mode)
# Mark queue task as done
self.token_queue.task_done()
async def _handle_token(
self, token_info: TokenInfo, marry_mode: bool, yolo_mode: bool
) -> None:
"""Handle a new token creation event.
@@ -168,6 +231,13 @@ class PumpTrader:
elif marry_mode:
logger.info("Marry mode enabled. Skipping sell operation.")
# Wait before looking for the next token
if yolo_mode:
logger.info(
f"YOLO mode enabled. Waiting {config.WAIT_TIME_BEFORE_NEW_TOKEN} seconds before looking for next token..."
)
await asyncio.sleep(config.WAIT_TIME_BEFORE_NEW_TOKEN)
except Exception as e:
logger.error(f"Error handling token {token_info.symbol}: {str(e)}")
@@ -211,7 +281,7 @@ class PumpTrader:
"symbol": token_info.symbol,
"price": price,
"amount": amount,
"tx_hash": tx_hash,
"tx_hash": str(tx_hash),
}
with open("trades/trades.log", "a") as log_file: