mirror of
https://github.com/chainstacklabs/pumpfun-bonkfun-bot.git
synced 2026-07-27 15:27:44 +00:00
feat: add support for multiple bots
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# This file defines comprehensive parameters and settings for the trading bot.
|
||||
# Carefully review and adjust values to match your trading strategy and risk tolerance.
|
||||
|
||||
# Bot identification and connection settings
|
||||
name: "bot-sniper-1"
|
||||
env_file: ".env"
|
||||
rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}"
|
||||
wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}"
|
||||
private_key: "${SOLANA_PRIVATE_KEY}"
|
||||
|
||||
enabled: true # You can turn off the bot w/o removing its config
|
||||
separate_process: true
|
||||
|
||||
# Geyser configuration (fastest method for getting updates)
|
||||
geyser:
|
||||
endpoint: "${GEYSER_ENDPOINT}"
|
||||
api_token: "${GEYSER_API_TOKEN}"
|
||||
|
||||
# Trading parameters
|
||||
# Control trade execution: amount of SOL per trade and acceptable price deviation
|
||||
trade:
|
||||
buy_amount: 0.0001 # Amount of SOL to spend when buying (in SOL)
|
||||
buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%)
|
||||
sell_slippage: 0.3
|
||||
|
||||
# EXTREME FAST mode configuration
|
||||
# When enabled, skips waiting for the bonding curve to stabilize and RPC price check.
|
||||
# The bot buys the specified number of tokens directly, making the process faster but less precise.
|
||||
extreme_fast_mode: true
|
||||
extreme_fast_token_amount: 20 # Amount of tokens to buy
|
||||
|
||||
# Priority fee configuration
|
||||
# Manage transaction speed and cost on the Solana network.
|
||||
# Note: dynamic mode requires an additional RPC call, which slows down the buying process.
|
||||
priority_fees:
|
||||
enable_dynamic: false # Use latest transactions to estimate required fee (getRecentPrioritizationFees)
|
||||
enable_fixed: true # Use fixed amount below
|
||||
fixed_amount: 1_000_000 # Base fee in microlamports
|
||||
extra_percentage: 0.0 # Percentage increase on riority fee regardless of the calculation method (0.1 = 10%)
|
||||
hard_cap: 1_000_000 # Maximum allowable fee in microlamports to prevent excessive spending
|
||||
|
||||
# Filters for token selection
|
||||
filters:
|
||||
match_string: null # Only process tokens with this string in name/symbol
|
||||
bro_address: null # Only trade tokens created by this user address
|
||||
listener_type: "geyser" # Method for detecting new tokens: "logs", "blocks", or "geyser"
|
||||
max_token_age: 0.001 # Maximum token age in seconds for processing
|
||||
marry_mode: false # Only buy tokens, skip selling
|
||||
yolo_mode: false # Continuously trade tokens
|
||||
|
||||
# Retry and timeout settings
|
||||
retries:
|
||||
max_attempts: 1 # Number of attempts for transaction submission
|
||||
wait_after_creation: 15 # Seconds to wait after token creation (only if EXTREME FAST is disabled)
|
||||
wait_after_buy: 15 # Holding period after buy transaction
|
||||
wait_before_new_token: 15 # Pause between token trades
|
||||
|
||||
# Token and account management
|
||||
cleanup:
|
||||
# Cleanup mode determines when to manage token accounts. Options:
|
||||
# "disabled": no cleanup will occur.
|
||||
# "on_fail": only clean up if a buy transaction fails.
|
||||
# "after_sell": clean up after selling.
|
||||
# "post_session": clean up all empty accounts after a trading session ends.
|
||||
mode: "post_session"
|
||||
force_close_with_burn: false # Force burning remaining tokens before closing account
|
||||
with_priority_fee: false # Use priority fees for cleanup transactions
|
||||
|
||||
# Node provider configuration (not implemented)
|
||||
node:
|
||||
max_rps: 25 # Maximum requests per second
|
||||
@@ -0,0 +1,71 @@
|
||||
# This file defines comprehensive parameters and settings for the trading bot.
|
||||
# Carefully review and adjust values to match your trading strategy and risk tolerance.
|
||||
|
||||
# Bot identification and connection settings
|
||||
name: "bot-sniper-2"
|
||||
env_file: ".env"
|
||||
rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}"
|
||||
wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}"
|
||||
private_key: "${SOLANA_PRIVATE_KEY}"
|
||||
|
||||
enabled: false # You can turn off the bot w/o removing its config
|
||||
separate_process: true
|
||||
|
||||
# Geyser configuration (fastest method for getting updates)
|
||||
geyser:
|
||||
endpoint: "${GEYSER_ENDPOINT}"
|
||||
api_token: "${GEYSER_API_TOKEN}"
|
||||
|
||||
# Trading parameters
|
||||
# Control trade execution: amount of SOL per trade and acceptable price deviation
|
||||
trade:
|
||||
buy_amount: 0.0001 # Amount of SOL to spend when buying (in SOL)
|
||||
buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%)
|
||||
sell_slippage: 0.3
|
||||
|
||||
# EXTREME FAST mode configuration
|
||||
# When enabled, skips waiting for the bonding curve to stabilize and RPC price check.
|
||||
# The bot buys the specified number of tokens directly, making the process faster but less precise.
|
||||
extreme_fast_mode: true
|
||||
extreme_fast_token_amount: 20 # Amount of tokens to buy
|
||||
|
||||
# Priority fee configuration
|
||||
# Manage transaction speed and cost on the Solana network.
|
||||
# Note: dynamic mode requires an additional RPC call, which slows down the buying process.
|
||||
priority_fees:
|
||||
enable_dynamic: false # Use latest transactions to estimate required fee (getRecentPrioritizationFees)
|
||||
enable_fixed: true # Use fixed amount below
|
||||
fixed_amount: 200_000 # Base fee in microlamports
|
||||
extra_percentage: 0.0 # Percentage increase on riority fee regardless of the calculation method (0.1 = 10%)
|
||||
hard_cap: 200_000 # Maximum allowable fee in microlamports to prevent excessive spending
|
||||
|
||||
# Filters for token selection
|
||||
filters:
|
||||
match_string: null # Only process tokens with this string in name/symbol
|
||||
bro_address: null # Only trade tokens created by this user address
|
||||
listener_type: "logs" # Method for detecting new tokens: "logs", "blocks", or "geyser"
|
||||
max_token_age: 0.001 # Maximum token age in seconds for processing
|
||||
marry_mode: false # Only buy tokens, skip selling
|
||||
yolo_mode: false # Continuously trade tokens
|
||||
|
||||
# Retry and timeout settings
|
||||
retries:
|
||||
max_attempts: 1 # Number of attempts for transaction submission
|
||||
wait_after_creation: 15 # Seconds to wait after token creation (only if EXTREME FAST is disabled)
|
||||
wait_after_buy: 15 # Holding period after buy transaction
|
||||
wait_before_new_token: 15 # Pause between token trades
|
||||
|
||||
# Token and account management
|
||||
cleanup:
|
||||
# Cleanup mode determines when to manage token accounts. Options:
|
||||
# "disabled": no cleanup will occur.
|
||||
# "on_fail": only clean up if a buy transaction fails.
|
||||
# "after_sell": clean up after selling.
|
||||
# "post_session": clean up all empty accounts after a trading session ends.
|
||||
mode: "post_session"
|
||||
force_close_with_burn: false # Force burning remaining tokens before closing account
|
||||
with_priority_fee: false # Use priority fees for cleanup transactions
|
||||
|
||||
# Node provider configuration (not implemented)
|
||||
node:
|
||||
max_rps: 25 # Maximum requests per second
|
||||
+2
-1
@@ -18,6 +18,7 @@ dependencies = [
|
||||
"grpcio>=1.71.0",
|
||||
"grpcio-tools>=1.71.0",
|
||||
"protobuf>=5.29.4",
|
||||
"pyyaml>=6.0.2",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
@@ -26,7 +27,7 @@ dev = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
pump_bot = "cli:sync_main"
|
||||
pump_bot = "bot_runner:main"
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import multiprocessing
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from config_loader import load_bot_config, print_config_summary
|
||||
from trading.trader import PumpTrader
|
||||
from utils.logger import setup_file_logging
|
||||
|
||||
|
||||
def setup_logging(bot_name: str):
|
||||
"""
|
||||
Set up logging to file for a specific bot instance.
|
||||
|
||||
Args:
|
||||
bot_name: Name of the bot for the log file
|
||||
"""
|
||||
log_dir = Path("logs")
|
||||
log_dir.mkdir(exist_ok=True)
|
||||
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
log_filename = log_dir / f"{bot_name}_{timestamp}.log"
|
||||
|
||||
setup_file_logging(str(log_filename))
|
||||
|
||||
async def start_bot(config_path: str):
|
||||
"""
|
||||
Start a trading bot with the configuration from the specified path.
|
||||
|
||||
Args:
|
||||
config_path: Path to the YAML configuration file
|
||||
"""
|
||||
cfg = load_bot_config(config_path)
|
||||
setup_logging(cfg["name"])
|
||||
print_config_summary(cfg)
|
||||
|
||||
trader = PumpTrader(
|
||||
# Connection settings
|
||||
rpc_endpoint=cfg["rpc_endpoint"],
|
||||
wss_endpoint=cfg["wss_endpoint"],
|
||||
private_key=cfg["private_key"],
|
||||
|
||||
# Trade parameters
|
||||
buy_amount=cfg["trade"]["buy_amount"],
|
||||
buy_slippage=cfg["trade"]["buy_slippage"],
|
||||
sell_slippage=cfg["trade"]["sell_slippage"],
|
||||
|
||||
# Extreme fast mode settings
|
||||
extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False),
|
||||
extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30),
|
||||
|
||||
# Listener configuration
|
||||
listener_type=cfg["filters"]["listener_type"],
|
||||
|
||||
# Geyser configuration (if applicable)
|
||||
geyser_endpoint=cfg.get("geyser", {}).get("endpoint"),
|
||||
geyser_api_token=cfg.get("geyser", {}).get("api_token"),
|
||||
|
||||
# Priority fee configuration
|
||||
enable_dynamic_priority_fee=cfg.get("priority_fees", {}).get("enable_dynamic", False),
|
||||
enable_fixed_priority_fee=cfg.get("priority_fees", {}).get("enable_fixed", True),
|
||||
fixed_priority_fee=cfg.get("priority_fees", {}).get("fixed_amount", 500000),
|
||||
extra_priority_fee=cfg.get("priority_fees", {}).get("extra_percentage", 0.0),
|
||||
hard_cap_prior_fee=cfg.get("priority_fees", {}).get("hard_cap", 500000),
|
||||
|
||||
# Retry and timeout settings
|
||||
max_retries=cfg.get("retries", {}).get("max_attempts", 10),
|
||||
wait_time_after_creation=cfg.get("retries", {}).get("wait_after_creation", 15),
|
||||
wait_time_after_buy=cfg.get("retries", {}).get("wait_after_buy", 15),
|
||||
wait_time_before_new_token=cfg.get("retries", {}).get("wait_before_new_token", 15),
|
||||
max_token_age=cfg.get("timing", {}).get("max_token_age", 0.001),
|
||||
token_wait_timeout=cfg.get("timing", {}).get("token_wait_timeout", 30),
|
||||
|
||||
# Cleanup settings
|
||||
cleanup_mode=cfg.get("cleanup", {}).get("mode", "disabled"),
|
||||
cleanup_force_close_with_burn=cfg.get("cleanup", {}).get("force_close_with_burn", False),
|
||||
cleanup_with_priority_fee=cfg.get("cleanup", {}).get("with_priority_fee", False),
|
||||
|
||||
# Trading filters
|
||||
match_string=cfg["filters"].get("match_string"),
|
||||
bro_address=cfg["filters"].get("bro_address"),
|
||||
marry_mode=cfg["filters"].get("marry_mode", False),
|
||||
yolo_mode=cfg["filters"].get("yolo_mode", False),
|
||||
)
|
||||
|
||||
await trader.start()
|
||||
|
||||
def run_all_bots():
|
||||
"""
|
||||
Run all bots defined in YAML files in the 'bots' directory.
|
||||
Only runs bots that have enabled=True (or where enabled is not specified).
|
||||
Bots can be run in separate processes based on their configuration.
|
||||
"""
|
||||
bot_dir = Path("bots")
|
||||
if not bot_dir.exists():
|
||||
logging.error(f"Bot directory '{bot_dir}' not found")
|
||||
return
|
||||
|
||||
bot_files = list(bot_dir.glob("*.yaml"))
|
||||
if not bot_files:
|
||||
logging.error(f"No bot configuration files found in '{bot_dir}'")
|
||||
return
|
||||
|
||||
logging.info(f"Found {len(bot_files)} bot configuration files")
|
||||
|
||||
processes = []
|
||||
skipped_bots = 0
|
||||
|
||||
for file in bot_files:
|
||||
try:
|
||||
cfg = load_bot_config(str(file))
|
||||
bot_name = cfg.get("name", file.stem)
|
||||
|
||||
# Skip bots with enabled=False
|
||||
if not cfg.get("enabled", True):
|
||||
logging.info(f"Skipping disabled bot '{bot_name}'")
|
||||
skipped_bots += 1
|
||||
continue
|
||||
|
||||
if cfg.get("separate_process", False):
|
||||
logging.info(f"Starting bot '{bot_name}' in separate process")
|
||||
p = multiprocessing.Process(
|
||||
target=lambda path=str(file): asyncio.run(start_bot(path)),
|
||||
name=f"bot-{bot_name}"
|
||||
)
|
||||
p.start()
|
||||
processes.append(p)
|
||||
else:
|
||||
logging.info(f"Starting bot '{bot_name}' in main process")
|
||||
asyncio.run(start_bot(str(file)))
|
||||
except Exception as e:
|
||||
logging.exception(f"Failed to start bot from {file}: {e}")
|
||||
|
||||
logging.info(f"Started {len(bot_files) - skipped_bots} bots, skipped {skipped_bots} disabled bots")
|
||||
|
||||
for p in processes:
|
||||
p.join()
|
||||
logging.info(f"Process {p.name} completed")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
run_all_bots()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,7 +1,8 @@
|
||||
import asyncio
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
from spl.token.instructions import BurnParams, CloseAccountParams, burn, close_account
|
||||
|
||||
from config import CLEANUP_FORCE_CLOSE_WITH_BURN
|
||||
from core.client import SolanaClient
|
||||
from core.priority_fee.manager import PriorityFeeManager
|
||||
from core.pubkeys import SystemAddresses
|
||||
@@ -19,6 +20,7 @@ class AccountCleanupManager:
|
||||
wallet: Wallet,
|
||||
priority_fee_manager: PriorityFeeManager,
|
||||
use_priority_fee: bool = False,
|
||||
force_burn: bool = False,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
@@ -29,6 +31,7 @@ class AccountCleanupManager:
|
||||
self.wallet = wallet
|
||||
self.priority_fee_manager = priority_fee_manager
|
||||
self.use_priority_fee = use_priority_fee
|
||||
self.close_with_force_burn = force_burn
|
||||
|
||||
async def cleanup_ata(self, mint: Pubkey) -> None:
|
||||
"""
|
||||
@@ -44,6 +47,9 @@ class AccountCleanupManager:
|
||||
else None
|
||||
)
|
||||
|
||||
logger.info("Waiting for 15 seconds for RPC node to synchronize...")
|
||||
await asyncio.sleep(15)
|
||||
|
||||
try:
|
||||
info = await solana_client.get_account_info(ata, encoding="base64")
|
||||
if not info.value:
|
||||
@@ -53,7 +59,7 @@ class AccountCleanupManager:
|
||||
balance = await self.client.get_token_account_balance(ata)
|
||||
instructions = []
|
||||
|
||||
if balance > 0 and CLEANUP_FORCE_CLOSE_WITH_BURN:
|
||||
if balance > 0 and self.close_with_force_burn:
|
||||
logger.info(f"Burning {balance} tokens from ATA {ata} (mint: {mint})...")
|
||||
burn_ix = burn(
|
||||
BurnParams(
|
||||
|
||||
+21
-16
@@ -1,37 +1,42 @@
|
||||
from cleanup.manager import AccountCleanupManager
|
||||
from config import CLEANUP_MODE, CLEANUP_WITH_PRIORITY_FEE
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def should_cleanup_after_failure() -> bool:
|
||||
return CLEANUP_MODE == "on_fail"
|
||||
def should_cleanup_after_failure(cleanup_mode) -> bool:
|
||||
return cleanup_mode == "on_fail"
|
||||
|
||||
|
||||
def should_cleanup_after_sell() -> bool:
|
||||
return CLEANUP_MODE == "after_sell"
|
||||
def should_cleanup_after_sell(cleanup_mode) -> bool:
|
||||
return cleanup_mode == "after_sell"
|
||||
|
||||
|
||||
def should_cleanup_post_session() -> bool:
|
||||
return CLEANUP_MODE == "post_session"
|
||||
def should_cleanup_post_session(cleanup_mode) -> bool:
|
||||
return cleanup_mode == "post_session"
|
||||
|
||||
|
||||
async def handle_cleanup_after_failure(client, wallet, mint, priority_fee_manager):
|
||||
if should_cleanup_after_failure():
|
||||
async def handle_cleanup_after_failure(
|
||||
client, wallet, mint, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn
|
||||
):
|
||||
if should_cleanup_after_failure(cleanup_mode):
|
||||
logger.info("[Cleanup] Triggered by failed buy transaction.")
|
||||
manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE)
|
||||
manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn)
|
||||
await manager.cleanup_ata(mint)
|
||||
|
||||
async def handle_cleanup_after_sell(client, wallet, mint, priority_fee_manager):
|
||||
if should_cleanup_after_sell():
|
||||
async def handle_cleanup_after_sell(
|
||||
client, wallet, mint, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn
|
||||
):
|
||||
if should_cleanup_after_sell(cleanup_mode):
|
||||
logger.info("[Cleanup] Triggered after token sell.")
|
||||
manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE)
|
||||
manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn)
|
||||
await manager.cleanup_ata(mint)
|
||||
|
||||
async def handle_cleanup_post_session(client, wallet, mints, priority_fee_manager):
|
||||
if should_cleanup_post_session():
|
||||
async def handle_cleanup_post_session(
|
||||
client, wallet, mints, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn
|
||||
):
|
||||
if should_cleanup_post_session(cleanup_mode):
|
||||
logger.info("[Cleanup] Triggered post trading session.")
|
||||
manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE)
|
||||
manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn)
|
||||
for mint in mints:
|
||||
await manager.cleanup_ata(mint)
|
||||
|
||||
-142
@@ -1,142 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Command-line interface for the pump.fun trading bot.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
import config
|
||||
from trading.trader import PumpTrader
|
||||
from utils.logger import get_logger, setup_file_logging
|
||||
|
||||
load_dotenv()
|
||||
|
||||
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})",
|
||||
)
|
||||
|
||||
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 = os.environ.get("SOLANA_NODE_RPC_ENDPOINT")
|
||||
wss_endpoint: str | None = os.environ.get("SOLANA_NODE_WSS_ENDPOINT")
|
||||
private_key: str | None = os.environ.get("SOLANA_PRIVATE_KEY")
|
||||
geyser_endpoint: str | None = os.environ.get("GEYSER_ENDPOINT")
|
||||
geyser_api_token: str | None = os.environ.get("GEYSER_API_TOKEN")
|
||||
|
||||
|
||||
# Validate configuration values
|
||||
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 not 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)
|
||||
|
||||
if config.LISTENER_TYPE.lower() == "geyser":
|
||||
if not geyser_endpoint:
|
||||
logger.error("GEYSER_ENDPOINT environment variable is not set")
|
||||
sys.exit(1)
|
||||
if not geyser_api_token:
|
||||
logger.error("GEYSER_API_TOKEN environment variable is not set")
|
||||
sys.exit(1)
|
||||
|
||||
# Get trading parameters
|
||||
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,
|
||||
listener_type=config.LISTENER_TYPE,
|
||||
geyser_endpoint=geyser_endpoint,
|
||||
geyser_api_token=geyser_api_token,
|
||||
)
|
||||
|
||||
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: {e!s}")
|
||||
finally:
|
||||
try:
|
||||
await trader.solana_client.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def sync_main() -> None:
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
-112
@@ -1,112 +0,0 @@
|
||||
"""
|
||||
Configuration for the pump.fun trading bot.
|
||||
|
||||
This file defines comprehensive parameters and settings for the trading bot.
|
||||
Carefully review and adjust values to match your trading strategy and risk tolerance.
|
||||
"""
|
||||
|
||||
# Trading parameters
|
||||
# Control trade execution: amount of SOL per trade and acceptable price deviation
|
||||
BUY_AMOUNT: int | float = 0.000_001 # Amount of SOL to spend when buying
|
||||
BUY_SLIPPAGE: float = 0.4 # Maximum acceptable price deviation (0.4 = 40%)
|
||||
SELL_SLIPPAGE: float = 0.4 # Consistent slippage tolerance to maintain trading strategy
|
||||
|
||||
|
||||
# EXTREME FAST Mode configuration
|
||||
# When enabled, skips waiting for the bonding curve to stabilize and RPC price check.
|
||||
# The bot buys the specified number of tokens directly, making the process faster but less precise.
|
||||
EXTREME_FAST_MODE: bool = True
|
||||
# Amount of tokens to buy in EXTREME FAST mode. No price calculation is done; the bot buys exactly this amount.
|
||||
EXTREME_FAST_TOKEN_AMOUNT: int = 30
|
||||
|
||||
|
||||
# Priority fee configuration
|
||||
# Manage transaction speed and cost on the Solana network
|
||||
ENABLE_DYNAMIC_PRIORITY_FEE: bool = False # Adaptive fee calculation
|
||||
ENABLE_FIXED_PRIORITY_FEE: bool = True # Use consistent, predictable fee
|
||||
FIXED_PRIORITY_FEE: int = 500_000 # Base fee in microlamports
|
||||
EXTRA_PRIORITY_FEE: float = 0.0 # Percentage increase on priority fee (0.1 = 10%)
|
||||
HARD_CAP_PRIOR_FEE: int = 500_000 # Maximum allowable fee to prevent excessive spending in microlamports
|
||||
|
||||
|
||||
# Listener configuration
|
||||
# Choose method for detecting new tokens on the network
|
||||
# "logs": Recommended for more stable token detection
|
||||
# "blocks": Unstable method, potentially less reliable
|
||||
# "geyser": The fastest way for getting updates, requires Geyser endpoint
|
||||
LISTENER_TYPE = "logs"
|
||||
|
||||
|
||||
# Retry and timeout settings
|
||||
# Control bot resilience and transaction handling
|
||||
MAX_RETRIES: int = 10 # Number of attempts for transaction submission
|
||||
|
||||
# Waiting periods in seconds between actions (TODO: to be replaced with retry mechanism)
|
||||
WAIT_TIME_AFTER_CREATION: int | float = 15 # Seconds to wait after token creation
|
||||
WAIT_TIME_AFTER_BUY: int | float = 15 # Holding period after buy transaction
|
||||
WAIT_TIME_BEFORE_NEW_TOKEN: int | float = 15 # Pause between token trades
|
||||
|
||||
|
||||
# Token and account management
|
||||
# Control token processing and account cleanup strategies
|
||||
MAX_TOKEN_AGE: int | float = 0.001 # Maximum token age in seconds for processing
|
||||
|
||||
# Cleanup mode determines when to manage token accounts. Options:
|
||||
# "disabled": No cleanup will occur.
|
||||
# "on_fail": Only clean up if a buy transaction fails.
|
||||
# "after_sell": Clean up after selling, but only if the balance is zero.
|
||||
# "post_session": Clean up all empty accounts after a trading session ends.
|
||||
CLEANUP_MODE: str = "disabled"
|
||||
CLEANUP_FORCE_CLOSE_WITH_BURN: bool = False # Burn remaining tokens before closing account, else skip ATA with non-zero balances
|
||||
CLEANUP_WITH_PRIORITY_FEE: bool = False # Use priority fees for cleanup transactions
|
||||
|
||||
|
||||
# Node provider configuration (TODO: to be implemented)
|
||||
# Manage RPC node interaction to prevent rate limiting
|
||||
MAX_RPS: int = 25 # Maximum requests per second
|
||||
|
||||
|
||||
def validate_configuration() -> None:
|
||||
"""
|
||||
Comprehensive validation of bot configuration.
|
||||
|
||||
Checks:
|
||||
- Type correctness
|
||||
- Value ranges
|
||||
- Logical consistency of settings
|
||||
"""
|
||||
# Configuration validation checks
|
||||
config_checks = [
|
||||
# (value, type, min_value, max_value, error_message)
|
||||
(BUY_AMOUNT, (int, float), 0, float('inf'), "BUY_AMOUNT must be a positive number"),
|
||||
(BUY_SLIPPAGE, float, 0, 1, "BUY_SLIPPAGE must be between 0 and 1"),
|
||||
(SELL_SLIPPAGE, float, 0, 1, "SELL_SLIPPAGE must be between 0 and 1"),
|
||||
(FIXED_PRIORITY_FEE, int, 0, float('inf'), "FIXED_PRIORITY_FEE must be a non-negative integer"),
|
||||
(EXTRA_PRIORITY_FEE, float, 0, 1, "EXTRA_PRIORITY_FEE must be between 0 and 1"),
|
||||
(HARD_CAP_PRIOR_FEE, int, 0, float('inf'), "HARD_CAP_PRIOR_FEE must be a non-negative integer"),
|
||||
(MAX_RETRIES, int, 0, 100, "MAX_RETRIES must be between 0 and 100")
|
||||
]
|
||||
|
||||
for value, expected_type, min_val, max_val, error_msg in config_checks:
|
||||
if not isinstance(value, expected_type):
|
||||
raise ValueError(f"Type error: {error_msg}")
|
||||
|
||||
if isinstance(value, (int, float)) and not (min_val <= value <= max_val):
|
||||
raise ValueError(f"Range error: {error_msg}")
|
||||
|
||||
# Logical consistency checks
|
||||
if ENABLE_DYNAMIC_PRIORITY_FEE and ENABLE_FIXED_PRIORITY_FEE:
|
||||
raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously")
|
||||
|
||||
# Validate listener type
|
||||
if LISTENER_TYPE not in ["logs", "blocks", "geyser"]:
|
||||
raise ValueError("LISTENER_TYPE must be either 'logs' or 'blocks'")
|
||||
|
||||
# Validate cleanup mode
|
||||
valid_cleanup_modes = ["disabled", "on_fail", "after_sell", "post_session"]
|
||||
if CLEANUP_MODE not in valid_cleanup_modes:
|
||||
raise ValueError(f"CLEANUP_MODE must be one of {valid_cleanup_modes}")
|
||||
|
||||
|
||||
# Validate configuration on import
|
||||
validate_configuration()
|
||||
@@ -0,0 +1,188 @@
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from dotenv import load_dotenv
|
||||
|
||||
REQUIRED_FIELDS = [
|
||||
"name", "rpc_endpoint", "wss_endpoint", "private_key",
|
||||
"trade.buy_amount", "trade.buy_slippage", "trade.sell_slippage",
|
||||
"filters.listener_type", "filters.max_token_age"
|
||||
]
|
||||
|
||||
CONFIG_VALIDATION_RULES = [
|
||||
# (path, type, min_value, max_value, error_message)
|
||||
("trade.buy_amount", (int, float), 0, float('inf'), "trade.buy_amount must be a positive number"),
|
||||
("trade.buy_slippage", float, 0, 1, "trade.buy_slippage must be between 0 and 1"),
|
||||
("trade.sell_slippage", float, 0, 1, "trade.sell_slippage must be between 0 and 1"),
|
||||
("priority_fees.fixed_amount", int, 0, float('inf'), "priority_fees.fixed_amount must be a non-negative integer"),
|
||||
("priority_fees.extra_percentage", float, 0, 1, "priority_fees.extra_percentage must be between 0 and 1"),
|
||||
("priority_fees.hard_cap", int, 0, float('inf'), "priority_fees.hard_cap must be a non-negative integer"),
|
||||
("retries.max_attempts", int, 0, 100, "retries.max_attempts must be between 0 and 100"),
|
||||
("filters.max_token_age", (int, float), 0, float('inf'), "filters.max_token_age must be a non-negative number")
|
||||
]
|
||||
|
||||
# Valid values for enum-like fields
|
||||
VALID_VALUES = {
|
||||
"filters.listener_type": ["logs", "blocks", "geyser"],
|
||||
"cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"]
|
||||
}
|
||||
|
||||
|
||||
def load_bot_config(path: str) -> dict:
|
||||
"""
|
||||
Load and validate a bot configuration from a YAML file.
|
||||
|
||||
Args:
|
||||
path: Path to the YAML configuration file (relative or absolute)
|
||||
|
||||
Returns:
|
||||
Validated configuration dictionary
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the configuration file doesn't exist
|
||||
ValueError: If the configuration is invalid
|
||||
"""
|
||||
with open(path) as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
env_file = config.get("env_file")
|
||||
if env_file:
|
||||
env_path = os.path.join(os.path.dirname(path), env_file)
|
||||
if os.path.exists(env_path):
|
||||
load_dotenv(env_path)
|
||||
else:
|
||||
# If not found relative to config, try relative to current working directory
|
||||
load_dotenv(env_file)
|
||||
|
||||
resolve_env_vars(config)
|
||||
validate_config(config)
|
||||
|
||||
return config
|
||||
|
||||
def resolve_env_vars(config: dict) -> None:
|
||||
"""
|
||||
Recursively resolve environment variables in the configuration.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary to process
|
||||
"""
|
||||
def resolve_env(value):
|
||||
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
|
||||
env_var = value[2:-1]
|
||||
env_value = os.getenv(env_var)
|
||||
if env_value is None:
|
||||
raise ValueError(f"Environment variable '{env_var}' not found")
|
||||
return env_value
|
||||
return value
|
||||
|
||||
def resolve_all(d):
|
||||
for k, v in d.items():
|
||||
if isinstance(v, dict):
|
||||
resolve_all(v)
|
||||
else:
|
||||
d[k] = resolve_env(v)
|
||||
|
||||
resolve_all(config)
|
||||
|
||||
def get_nested_value(config: dict, path: str) -> Any:
|
||||
"""
|
||||
Get a nested value from the configuration using dot notation.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary
|
||||
path: Path to the value using dot notation (e.g., "trade.buy_amount")
|
||||
|
||||
Returns:
|
||||
The value at the specified path
|
||||
|
||||
Raises:
|
||||
ValueError: If the path doesn't exist in the configuration
|
||||
"""
|
||||
keys = path.split(".")
|
||||
value = config
|
||||
for key in keys:
|
||||
if not isinstance(value, dict) or key not in value:
|
||||
raise ValueError(f"Missing required config key: {path}")
|
||||
value = value[key]
|
||||
return value
|
||||
|
||||
def validate_config(config: dict) -> None:
|
||||
"""
|
||||
Validate the configuration against defined rules.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary to validate
|
||||
|
||||
Raises:
|
||||
ValueError: If the configuration is invalid
|
||||
"""
|
||||
for field in REQUIRED_FIELDS:
|
||||
get_nested_value(config, field)
|
||||
|
||||
for path, expected_type, min_val, max_val, error_msg in CONFIG_VALIDATION_RULES:
|
||||
try:
|
||||
value = get_nested_value(config, path)
|
||||
|
||||
if not isinstance(value, expected_type):
|
||||
raise ValueError(f"Type error: {error_msg}")
|
||||
|
||||
if isinstance(value, (int, float)) and not (min_val <= value <= max_val):
|
||||
raise ValueError(f"Range error: {error_msg}")
|
||||
|
||||
except ValueError as e:
|
||||
# Re-raise if it's our own error
|
||||
if str(e).startswith(("Type error:", "Range error:")):
|
||||
raise
|
||||
# Otherwise, the field might be missing
|
||||
continue
|
||||
|
||||
# Validate enum-like fields
|
||||
for path, valid_values in VALID_VALUES.items():
|
||||
try:
|
||||
value = get_nested_value(config, path)
|
||||
if value not in valid_values:
|
||||
raise ValueError(f"{path} must be one of {valid_values}")
|
||||
except ValueError:
|
||||
# Skip if the field is missing
|
||||
continue
|
||||
|
||||
# Cannot enable both dynamic and fixed priority fees
|
||||
try:
|
||||
dynamic = get_nested_value(config, "priority_fees.enable_dynamic")
|
||||
fixed = get_nested_value(config, "priority_fees.enable_fixed")
|
||||
if dynamic and fixed:
|
||||
raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously")
|
||||
except ValueError:
|
||||
# Skip if one of the fields is missing
|
||||
pass
|
||||
|
||||
def print_config_summary(config: dict) -> None:
|
||||
"""
|
||||
Print a summary of the loaded configuration.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary
|
||||
"""
|
||||
print(f"Bot name: {config.get('name', 'unnamed')}")
|
||||
print(f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}")
|
||||
|
||||
trade = config.get('trade', {})
|
||||
print("Trade settings:")
|
||||
print(f" - Buy amount: {trade.get('buy_amount', 'not configured')} SOL")
|
||||
print(f" - Buy slippage: {trade.get('buy_slippage', 'not configured') * 100}%")
|
||||
print(f" - Extreme fast mode: {'enabled' if trade.get('extreme_fast_mode') else 'disabled'}")
|
||||
|
||||
fees = config.get('priority_fees', {})
|
||||
print("Priority fees:")
|
||||
if fees.get('enable_dynamic'):
|
||||
print(" - Dynamic fees enabled")
|
||||
elif fees.get('enable_fixed'):
|
||||
print(f" - Fixed fee: {fees.get('fixed_amount', 'not configured')} microlamports")
|
||||
|
||||
print("Configuration loaded successfully!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = load_bot_config("bots/bot-sniper.yaml")
|
||||
print_config_summary(config)
|
||||
+3
-2
@@ -47,7 +47,8 @@ class SolanaClient:
|
||||
self._cached_blockhash = blockhash
|
||||
except Exception as e:
|
||||
logger.warning(f"Blockhash fetch failed: {e!s}")
|
||||
await asyncio.sleep(interval)
|
||||
finally:
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
async def get_cached_blockhash(self) -> Hash:
|
||||
"""Return the most recently cached blockhash."""
|
||||
@@ -195,7 +196,7 @@ class SolanaClient:
|
||||
"""
|
||||
client = await self.get_client()
|
||||
try:
|
||||
await client.confirm_transaction(signature, commitment=commitment)
|
||||
await client.confirm_transaction(signature, commitment=commitment, sleep_seconds=1)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to confirm transaction {signature}: {e!s}")
|
||||
|
||||
+352
-153
@@ -10,7 +10,6 @@ from datetime import datetime
|
||||
|
||||
from solders.pubkey import Pubkey
|
||||
|
||||
import config
|
||||
from cleanup.modes import (
|
||||
handle_cleanup_after_failure,
|
||||
handle_cleanup_after_sell,
|
||||
@@ -34,7 +33,6 @@ logger = get_logger(__name__)
|
||||
|
||||
class PumpTrader:
|
||||
"""Coordinates trading operations for pump.fun tokens with focus on freshness."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rpc_endpoint: str,
|
||||
@@ -43,13 +41,40 @@ class PumpTrader:
|
||||
buy_amount: float,
|
||||
buy_slippage: float,
|
||||
sell_slippage: float,
|
||||
max_retries: int = 5,
|
||||
listener_type: str = "logs",
|
||||
geyser_endpoint: str | None = None,
|
||||
geyser_api_token: str | None = None,
|
||||
|
||||
extreme_fast_mode: bool = False,
|
||||
extreme_fast_token_amount: int = 30,
|
||||
|
||||
# Priority fee configuration
|
||||
enable_dynamic_priority_fee: bool = False,
|
||||
enable_fixed_priority_fee: bool = True,
|
||||
fixed_priority_fee: int = 200_000,
|
||||
extra_priority_fee: float = 0.0,
|
||||
hard_cap_prior_fee: int = 200_000,
|
||||
|
||||
# Retry and timeout settings
|
||||
max_retries: int = 3,
|
||||
wait_time_after_creation: int = 15, # here and further - seconds
|
||||
wait_time_after_buy: int = 15,
|
||||
wait_time_before_new_token: int = 15,
|
||||
max_token_age: int | float = 0.001,
|
||||
token_wait_timeout: int = 30,
|
||||
|
||||
# Cleanup settings
|
||||
cleanup_mode: str = "disabled",
|
||||
cleanup_force_close_with_burn: bool = False,
|
||||
cleanup_with_priority_fee: bool = False,
|
||||
|
||||
# Trading filters
|
||||
match_string: str | None = None,
|
||||
bro_address: str | None = None,
|
||||
marry_mode: bool = False,
|
||||
yolo_mode: bool = False,
|
||||
):
|
||||
"""Initialize the pump trader.
|
||||
|
||||
Args:
|
||||
rpc_endpoint: RPC endpoint URL
|
||||
wss_endpoint: WebSocket endpoint URL
|
||||
@@ -57,24 +82,47 @@ class PumpTrader:
|
||||
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
|
||||
|
||||
listener_type: Type of listener to use ('logs', 'blocks', or 'geyser')
|
||||
geyser_endpoint: Geyser endpoint URL (required for geyser listener)
|
||||
geyser_api_token: Geyser API token (required for geyser listener)
|
||||
|
||||
extreme_fast_mode: Whether to enable extreme fast mode
|
||||
extreme_fast_token_amount: Maximum token amount for extreme fast mode
|
||||
|
||||
enable_dynamic_priority_fee: Whether to enable dynamic priority fees
|
||||
enable_fixed_priority_fee: Whether to enable fixed priority fees
|
||||
fixed_priority_fee: Fixed priority fee amount
|
||||
extra_priority_fee: Extra percentage for priority fees
|
||||
hard_cap_prior_fee: Hard cap for priority fees
|
||||
|
||||
max_retries: Maximum number of retry attempts
|
||||
wait_time_after_creation: Time to wait after token creation (seconds)
|
||||
wait_time_after_buy: Time to wait after buying a token (seconds)
|
||||
wait_time_before_new_token: Time to wait before processing a new token (seconds)
|
||||
max_token_age: Maximum age of token to process (seconds)
|
||||
token_wait_timeout: Timeout for waiting for a token in single-token mode (seconds)
|
||||
|
||||
cleanup_mode: Cleanup mode ("disabled", "auto", or "manual")
|
||||
cleanup_force_close_with_burn: Whether to force close with burn during cleanup
|
||||
cleanup_with_priority_fee: Whether to use priority fees during cleanup
|
||||
|
||||
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
|
||||
"""
|
||||
self.solana_client = SolanaClient(rpc_endpoint)
|
||||
self.wallet = Wallet(private_key)
|
||||
self.curve_manager = BondingCurveManager(self.solana_client)
|
||||
|
||||
self.priority_fee_manager = PriorityFeeManager(
|
||||
client=self.solana_client,
|
||||
enable_dynamic_fee=config.ENABLE_DYNAMIC_PRIORITY_FEE,
|
||||
enable_fixed_fee=config.ENABLE_FIXED_PRIORITY_FEE,
|
||||
fixed_fee=config.FIXED_PRIORITY_FEE,
|
||||
extra_fee=config.EXTRA_PRIORITY_FEE,
|
||||
hard_cap=config.HARD_CAP_PRIOR_FEE,
|
||||
enable_dynamic_fee=enable_dynamic_priority_fee,
|
||||
enable_fixed_fee=enable_fixed_priority_fee,
|
||||
fixed_fee=fixed_priority_fee,
|
||||
extra_fee=extra_priority_fee,
|
||||
hard_cap=hard_cap_prior_fee,
|
||||
)
|
||||
|
||||
self.buyer = TokenBuyer(
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
@@ -83,10 +131,9 @@ class PumpTrader:
|
||||
buy_amount,
|
||||
buy_slippage,
|
||||
max_retries,
|
||||
config.EXTREME_FAST_TOKEN_AMOUNT,
|
||||
config.EXTREME_FAST_MODE
|
||||
extreme_fast_token_amount,
|
||||
extreme_fast_mode
|
||||
)
|
||||
|
||||
self.seller = TokenSeller(
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
@@ -95,7 +142,7 @@ class PumpTrader:
|
||||
sell_slippage,
|
||||
max_retries,
|
||||
)
|
||||
|
||||
|
||||
# Initialize the appropriate listener type
|
||||
listener_type = listener_type.lower()
|
||||
if listener_type == "geyser":
|
||||
@@ -114,69 +161,166 @@ class PumpTrader:
|
||||
else:
|
||||
self.token_listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM)
|
||||
logger.info("Using blockSubscribe listener for token monitoring")
|
||||
|
||||
|
||||
# Trading parameters
|
||||
self.buy_amount = buy_amount
|
||||
self.buy_slippage = buy_slippage
|
||||
self.sell_slippage = sell_slippage
|
||||
self.max_retries = max_retries
|
||||
self.max_token_age = config.MAX_TOKEN_AGE
|
||||
self.extreme_fast_mode = extreme_fast_mode
|
||||
self.extreme_fast_token_amount = extreme_fast_token_amount
|
||||
|
||||
# Timing parameters
|
||||
self.wait_time_after_creation = wait_time_after_creation
|
||||
self.wait_time_after_buy = wait_time_after_buy
|
||||
self.wait_time_before_new_token = wait_time_before_new_token
|
||||
self.max_token_age = max_token_age
|
||||
self.token_wait_timeout = token_wait_timeout
|
||||
|
||||
# Cleanup parameters
|
||||
self.cleanup_mode = cleanup_mode
|
||||
self.cleanup_force_close_with_burn = cleanup_force_close_with_burn
|
||||
self.cleanup_with_priority_fee = cleanup_with_priority_fee
|
||||
|
||||
# Trading filters/modes
|
||||
self.match_string = match_string
|
||||
self.bro_address = bro_address
|
||||
self.marry_mode = marry_mode
|
||||
self.yolo_mode = yolo_mode
|
||||
|
||||
# State tracking
|
||||
self.traded_mints: set[Pubkey] = set()
|
||||
|
||||
# Token processing state
|
||||
self.token_queue = asyncio.Queue()
|
||||
self.processing = False
|
||||
self.token_queue: asyncio.Queue = asyncio.Queue()
|
||||
self.processing: bool = False
|
||||
self.processed_tokens: set[str] = set()
|
||||
self.token_timestamps: dict[str, float] = {}
|
||||
|
||||
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
|
||||
"""
|
||||
async def start(self) -> None:
|
||||
"""Start the trading bot and listen for new tokens."""
|
||||
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}")
|
||||
logger.info(f"Match filter: {self.match_string if self.match_string else 'None'}")
|
||||
logger.info(f"Creator filter: {self.bro_address if self.bro_address else 'None'}")
|
||||
logger.info(f"Marry mode: {self.marry_mode}")
|
||||
logger.info(f"YOLO mode: {self.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._queue_token(token),
|
||||
match_string,
|
||||
bro_address,
|
||||
)
|
||||
# Choose operating mode based on yolo_mode
|
||||
if not self.yolo_mode:
|
||||
# Single token mode: process one token and exit
|
||||
logger.info("Running in single token mode - will process one token and exit")
|
||||
token_info = await self._wait_for_token()
|
||||
if token_info:
|
||||
await self._handle_token(token_info)
|
||||
logger.info("Finished processing single token. Exiting...")
|
||||
else:
|
||||
logger.info(f"No suitable token found within timeout period ({self.token_wait_timeout}s). Exiting...")
|
||||
else:
|
||||
# Continuous mode: process tokens until interrupted
|
||||
logger.info("Running in continuous mode - will process tokens until interrupted")
|
||||
processor_task = asyncio.create_task(
|
||||
self._process_token_queue()
|
||||
)
|
||||
|
||||
try:
|
||||
await self.token_listener.listen_for_tokens(
|
||||
lambda token: self._queue_token(token),
|
||||
self.match_string,
|
||||
self.bro_address,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Token listening stopped due to error: {e!s}")
|
||||
finally:
|
||||
processor_task.cancel()
|
||||
try:
|
||||
await processor_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Trading stopped due to error: {e!s}")
|
||||
processor_task.cancel()
|
||||
await self.solana_client.close()
|
||||
|
||||
|
||||
finally:
|
||||
processor_task.cancel()
|
||||
if self.traded_mints:
|
||||
# Close ATA if enabled
|
||||
await handle_cleanup_post_session(self.solana_client, self.wallet, list(self.traded_mints), self.priority_fee_manager)
|
||||
await self.solana_client.close()
|
||||
await self._cleanup_resources()
|
||||
logger.info("Pump trader has shut down")
|
||||
|
||||
async def _queue_token(self, token_info: TokenInfo) -> None:
|
||||
"""Queue a token for processing if not already processed."""
|
||||
async def _wait_for_token(self) -> TokenInfo | None:
|
||||
"""Wait for a single token to be detected.
|
||||
|
||||
Returns:
|
||||
TokenInfo or None if timeout occurs
|
||||
"""
|
||||
# Create a one-time event to signal when a token is found
|
||||
token_found = asyncio.Event()
|
||||
found_token = None
|
||||
|
||||
async def token_callback(token: TokenInfo) -> None:
|
||||
nonlocal found_token
|
||||
token_key = str(token.mint)
|
||||
|
||||
# Only process if not already processed and fresh
|
||||
if token_key not in self.processed_tokens:
|
||||
# Record when the token was discovered
|
||||
self.token_timestamps[token_key] = asyncio.get_event_loop().time()
|
||||
found_token = token
|
||||
self.processed_tokens.add(token_key)
|
||||
token_found.set()
|
||||
|
||||
listener_task = asyncio.create_task(
|
||||
self.token_listener.listen_for_tokens(
|
||||
token_callback,
|
||||
self.match_string,
|
||||
self.bro_address,
|
||||
)
|
||||
)
|
||||
|
||||
# Wait for a token with a timeout
|
||||
try:
|
||||
logger.info(f"Waiting for a suitable token (timeout: {self.token_wait_timeout}s)...")
|
||||
await asyncio.wait_for(token_found.wait(), timeout=self.token_wait_timeout)
|
||||
logger.info(f"Found token: {found_token.symbol} ({found_token.mint})")
|
||||
return found_token
|
||||
except TimeoutError:
|
||||
logger.info(f"Timed out after waiting {self.token_wait_timeout}s for a token")
|
||||
return None
|
||||
finally:
|
||||
listener_task.cancel()
|
||||
try:
|
||||
await listener_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
async def _cleanup_resources(self) -> None:
|
||||
"""Perform cleanup operations before shutting down."""
|
||||
if self.traded_mints:
|
||||
try:
|
||||
logger.info(f"Cleaning up {len(self.traded_mints)} traded token(s)...")
|
||||
await handle_cleanup_post_session(
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
list(self.traded_mints),
|
||||
self.priority_fee_manager,
|
||||
self.cleanup_mode,
|
||||
self.cleanup_with_priority_fee,
|
||||
self.cleanup_force_close_with_burn
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error during cleanup: {e!s}")
|
||||
|
||||
old_keys = {k for k in self.token_timestamps if k not in self.processed_tokens}
|
||||
for key in old_keys:
|
||||
self.token_timestamps.pop(key, None)
|
||||
|
||||
await self.solana_client.close()
|
||||
|
||||
async def _queue_token(
|
||||
self, token_info: TokenInfo
|
||||
) -> None:
|
||||
"""Queue a token for processing if not already processed.
|
||||
|
||||
Args:
|
||||
token_info: Token information to queue
|
||||
"""
|
||||
token_key = str(token_info.mint)
|
||||
|
||||
if token_key in self.processed_tokens:
|
||||
@@ -189,126 +333,178 @@ class PumpTrader:
|
||||
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:
|
||||
async def _process_token_queue(self) -> None:
|
||||
"""Continuously process tokens from the queue, only if they're fresh."""
|
||||
while True:
|
||||
token_info = await self.token_queue.get()
|
||||
token_key = str(token_info.mint)
|
||||
try:
|
||||
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)"
|
||||
# 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
|
||||
|
||||
self.processed_tokens.add(token_key)
|
||||
|
||||
logger.info(
|
||||
f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)"
|
||||
)
|
||||
await self._handle_token(token_info)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
# Handle cancellation gracefully
|
||||
logger.info("Token queue processor was cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Error in token queue processor: {e!s}")
|
||||
finally:
|
||||
self.token_queue.task_done()
|
||||
continue
|
||||
|
||||
self.processed_tokens.add(token_key)
|
||||
|
||||
logger.info(
|
||||
f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)"
|
||||
)
|
||||
await self._handle_token(token_info, marry_mode, yolo_mode)
|
||||
|
||||
self.token_queue.task_done()
|
||||
|
||||
async def _handle_token(
|
||||
self, token_info: TokenInfo, marry_mode: bool, yolo_mode: bool
|
||||
self, token_info: TokenInfo
|
||||
) -> 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:
|
||||
# Save token info to file
|
||||
await self._save_token_info(token_info)
|
||||
|
||||
if not config.EXTREME_FAST_MODE:
|
||||
# Wait for bonding curve to stabilize (unless in extreme fast mode)
|
||||
if not self.extreme_fast_mode:
|
||||
logger.info(
|
||||
f"Waiting for {config.WAIT_TIME_AFTER_CREATION} seconds for the bonding curve to stabilize..."
|
||||
f"Waiting for {self.wait_time_after_creation} seconds for the bonding curve to stabilize..."
|
||||
)
|
||||
await asyncio.sleep(config.WAIT_TIME_AFTER_CREATION)
|
||||
await asyncio.sleep(self.wait_time_after_creation)
|
||||
|
||||
# Buy token
|
||||
logger.info(
|
||||
f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol}..."
|
||||
)
|
||||
buy_result: TradeResult = await self.buyer.execute(token_info)
|
||||
|
||||
if buy_result.success:
|
||||
logger.info(f"Successfully bought {token_info.symbol}")
|
||||
self._log_trade(
|
||||
"buy",
|
||||
token_info,
|
||||
buy_result.price, # type: ignore
|
||||
buy_result.amount, # type: ignore
|
||||
buy_result.tx_signature,
|
||||
)
|
||||
self.traded_mints.add(token_info.mint)
|
||||
await self._handle_successful_buy(token_info, buy_result)
|
||||
else:
|
||||
logger.error(
|
||||
f"Failed to buy {token_info.symbol}: {buy_result.error_message}"
|
||||
)
|
||||
# Close ATA if enabled
|
||||
await handle_cleanup_after_failure(self.solana_client, self.wallet, token_info.mint, self.priority_fee_manager)
|
||||
await self._handle_failed_buy(token_info, buy_result)
|
||||
|
||||
# Sell token if not in marry mode
|
||||
if not marry_mode and buy_result.success:
|
||||
# Only wait for next token in yolo mode
|
||||
if self.yolo_mode:
|
||||
logger.info(
|
||||
f"Waiting for {config.WAIT_TIME_AFTER_BUY} seconds before selling..."
|
||||
f"YOLO mode enabled. Waiting {self.wait_time_before_new_token} seconds before looking for next token..."
|
||||
)
|
||||
await asyncio.sleep(config.WAIT_TIME_AFTER_BUY)
|
||||
|
||||
logger.info(f"Selling {token_info.symbol}...")
|
||||
sell_result: TradeResult = await self.seller.execute(token_info)
|
||||
|
||||
if sell_result.success:
|
||||
logger.info(f"Successfully sold {token_info.symbol}")
|
||||
self._log_trade(
|
||||
"sell",
|
||||
token_info,
|
||||
sell_result.price, # type: ignore
|
||||
sell_result.amount, # type: ignore
|
||||
sell_result.tx_signature,
|
||||
)
|
||||
# Close ATA if enabled
|
||||
await handle_cleanup_after_sell(self.solana_client, self.wallet, token_info.mint, self.priority_fee_manager)
|
||||
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(
|
||||
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)
|
||||
await asyncio.sleep(self.wait_time_before_new_token)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling token {token_info.symbol}: {e!s}")
|
||||
|
||||
async def _save_token_info(self, token_info: TokenInfo) -> None:
|
||||
async def _handle_successful_buy(
|
||||
self, token_info: TokenInfo, buy_result: TradeResult
|
||||
) -> None:
|
||||
"""Handle successful token purchase.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
buy_result: The result of the buy operation
|
||||
"""
|
||||
logger.info(f"Successfully bought {token_info.symbol}")
|
||||
self._log_trade(
|
||||
"buy",
|
||||
token_info,
|
||||
buy_result.price, # type: ignore
|
||||
buy_result.amount, # type: ignore
|
||||
buy_result.tx_signature,
|
||||
)
|
||||
self.traded_mints.add(token_info.mint)
|
||||
|
||||
# Sell token if not in marry mode
|
||||
if not self.marry_mode:
|
||||
logger.info(
|
||||
f"Waiting for {self.wait_time_after_buy} seconds before selling..."
|
||||
)
|
||||
await asyncio.sleep(self.wait_time_after_buy)
|
||||
|
||||
logger.info(f"Selling {token_info.symbol}...")
|
||||
sell_result: TradeResult = await self.seller.execute(token_info)
|
||||
|
||||
if sell_result.success:
|
||||
logger.info(f"Successfully sold {token_info.symbol}")
|
||||
self._log_trade(
|
||||
"sell",
|
||||
token_info,
|
||||
sell_result.price, # type: ignore
|
||||
sell_result.amount, # type: ignore
|
||||
sell_result.tx_signature,
|
||||
)
|
||||
# Close ATA if enabled
|
||||
await handle_cleanup_after_sell(
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
token_info.mint,
|
||||
self.priority_fee_manager,
|
||||
self.cleanup_mode,
|
||||
self.cleanup_with_priority_fee,
|
||||
self.cleanup_force_close_with_burn
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"Failed to sell {token_info.symbol}: {sell_result.error_message}"
|
||||
)
|
||||
else:
|
||||
logger.info("Marry mode enabled. Skipping sell operation.")
|
||||
|
||||
async def _handle_failed_buy(
|
||||
self, token_info: TokenInfo, buy_result: TradeResult
|
||||
) -> None:
|
||||
"""Handle failed token purchase.
|
||||
|
||||
Args:
|
||||
token_info: Token information
|
||||
buy_result: The result of the buy operation
|
||||
"""
|
||||
logger.error(
|
||||
f"Failed to buy {token_info.symbol}: {buy_result.error_message}"
|
||||
)
|
||||
# Close ATA if enabled
|
||||
await handle_cleanup_after_failure(
|
||||
self.solana_client,
|
||||
self.wallet,
|
||||
token_info.mint,
|
||||
self.priority_fee_manager,
|
||||
self.cleanup_mode,
|
||||
self.cleanup_with_priority_fee,
|
||||
self.cleanup_force_close_with_burn
|
||||
)
|
||||
|
||||
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")
|
||||
try:
|
||||
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))
|
||||
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}")
|
||||
logger.info(f"Token information saved to {file_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save token information: {e!s}")
|
||||
|
||||
def _log_trade(
|
||||
self,
|
||||
@@ -327,17 +523,20 @@ class PumpTrader:
|
||||
amount: Trade amount in SOL
|
||||
tx_hash: Transaction hash
|
||||
"""
|
||||
os.makedirs("trades", exist_ok=True)
|
||||
try:
|
||||
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,
|
||||
"amount": amount,
|
||||
"tx_hash": str(tx_hash),
|
||||
}
|
||||
log_entry = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"action": action,
|
||||
"token_address": str(token_info.mint),
|
||||
"symbol": token_info.symbol,
|
||||
"price": price,
|
||||
"amount": amount,
|
||||
"tx_hash": str(tx_hash) if tx_hash else None,
|
||||
}
|
||||
|
||||
with open("trades/trades.log", "a") as log_file:
|
||||
log_file.write(json.dumps(log_entry) + "\n")
|
||||
with open("trades/trades.log", "a") as log_file:
|
||||
log_file.write(json.dumps(log_entry) + "\n")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to log trade information: {e!s}")
|
||||
+5
-11
@@ -3,7 +3,6 @@ Logging utilities for the pump.fun trading bot.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
# Global dict to store loggers
|
||||
_loggers: dict[str, logging.Logger] = {}
|
||||
@@ -27,16 +26,6 @@ def get_logger(name: str, level: int = logging.INFO) -> logging.Logger:
|
||||
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
|
||||
|
||||
@@ -52,6 +41,11 @@ def setup_file_logging(
|
||||
"""
|
||||
root_logger = logging.getLogger()
|
||||
|
||||
# Check if file handler with same filename already exists
|
||||
for handler in root_logger.handlers:
|
||||
if isinstance(handler, logging.FileHandler) and handler.baseFilename == filename:
|
||||
return # File handler already added
|
||||
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "RAKAMAKAFO",
|
||||
"symbol": "MAKAFO",
|
||||
"uri": "https://cf-ipfs.com/ipfs/QmYyNfffJL8aLzsQQBxSDEmbQ2STSPS35rfFwE7isjJiVu",
|
||||
"mint": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump",
|
||||
"bondingCurve": "9UznPeXvYtPqq8saSyM65cC8LCtyAKPnLCGG7Mq1pUTq",
|
||||
"associatedBondingCurve": "CTe6pwwgzEUvRmPGRE2EZXrESpiWawK5TwyJhgvFgJ6T",
|
||||
"user": "Ev5WEEJ2Y1qKGE8KCNvBNVEgy9fJUuttA93pMo2fcGFd"
|
||||
}
|
||||
+2
-12
@@ -1,12 +1,2 @@
|
||||
{"timestamp": "2024-08-22T10:51:30.306580", "action": "buy", "token_address": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", "price": 1.668947042812354e-07, "tx_hash": "XZDRuEnaBNEeuFAiGyJsqBmVF3uZvFqAQUKsZVKTxsThHxN43LiRqy3T4cSH4dRXGB7c5A4855iYQDLY4BNJJhP"}
|
||||
{"timestamp": "2024-08-22T10:51:56.116951", "action": "sell", "token_address": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", "price": 1.668947042812354e-07, "tx_hash": "WQHnEoyztfDf5FJyQRXDGxxtqf2EcMGs8SET43KuYU8Dpdunruku22SHnfiH2xsUi1hTBjC1msvTcnU78fFKrc2"}
|
||||
{"timestamp": "2025-03-06T21:53:16.417950", "action": "buy", "token_address": "7ztobvXwBd8UBn2mUuatjpmEmn7EZpifXv98xjCrpump", "symbol": "$Auti", "price": 3.3860819583417394e-08, "amount": 29.532657871333047, "tx_hash": "4fUuh16912cK7xiZsvY1scu5P5NtWGwowcAYXQvvr1FWAvPD57mPKfrq5PMMzMULhMiVn86fiDCUfwWt6B7NPNxV"}
|
||||
{"timestamp": "2025-03-06T21:53:33.704455", "action": "sell", "token_address": "7ztobvXwBd8UBn2mUuatjpmEmn7EZpifXv98xjCrpump", "symbol": "$Auti", "price": 3.38403327639918e-08, "amount": 29.532657, "tx_hash": "4TRFmKYkibrgn2fX4UT7mM8i7Lpu3FgXKvY1d6AsEUaxLj11JxWo61xxqYGW7B6wzdaY9LWVWZnTSDTZUUNs5pFU"}
|
||||
{"timestamp": "2025-03-06T22:04:01.067407", "action": "buy", "token_address": "5jch2xH14RBWZgKwmxEUJwC4howFNBCBqbnZ5uguGujj", "symbol": "$MOONWEN", "price": 2.8543914418196173e-08, "amount": 35.03373732659877, "tx_hash": "VCWbh79TmNWtt9kjex5LLtWKGHjc3nPBQVJdynC7B85nbnXx4DBgwguqAe6StYVAUS7jzP17pN5FWWGvbiUBRfk"}
|
||||
{"timestamp": "2025-03-06T22:04:48.843998", "action": "sell", "token_address": "5jch2xH14RBWZgKwmxEUJwC4howFNBCBqbnZ5uguGujj", "symbol": "$MOONWEN", "price": 2.8056120698010214e-08, "amount": 35.033737, "tx_hash": "RLx8hFVd5DVZdUg5vN41Fs8Yz66yTEUEvegasuHwYQ7DTJhPcCdggoNBtipb93VPVdtQLZydxc9ZKRZ1uzK6KzH"}
|
||||
{"timestamp": "2025-03-06T22:05:11.444317", "action": "buy", "token_address": "C9GrXnLPkD4fdj83ua7TFndtxvCVxdYyZzqrMBBZpump", "symbol": "CRYPTO ", "price": 3.383038210726932e-08, "amount": 29.559228649242023, "tx_hash": "3E4RT5kvRezWJmQKJ9wmcAiDcYZefohxFVNdEZPweE9vhq215rSmqBn4Dv4ypH5KUUuNJzKdhjyrGU2b47woWr9e"}
|
||||
{"timestamp": "2025-03-06T22:05:25.789876", "action": "sell", "token_address": "C9GrXnLPkD4fdj83ua7TFndtxvCVxdYyZzqrMBBZpump", "symbol": "CRYPTO ", "price": 3.3830384158620706e-08, "amount": 29.559228, "tx_hash": "3oyJVzsRvnQV5qBsEp4hSk97eFWzUjzfWWa16GMUXtMd7JBmbQkTkmvs7N8nagYcJvqfFhzhCKZEG6onxjYYQbwX"}
|
||||
{"timestamp": "2025-04-03T21:55:46.849123", "action": "buy", "token_address": "4rHSLfYLkFmHTcyCF8thnUmyuX4MBrHdjdftZXM7pump", "symbol": "GCT", "price": 3.3333333333333334e-08, "amount": 30, "tx_hash": "35UncvxypdvuKC4gD3jXnmmtwfBfEhFc2Y2JLfszzvHqsZumcjp2CMkXH4HD7iexgkPEpG1je73bwnFWbNQjxNUS"}
|
||||
{"timestamp": "2025-04-03T21:56:05.816803", "action": "sell", "token_address": "4rHSLfYLkFmHTcyCF8thnUmyuX4MBrHdjdftZXM7pump", "symbol": "GCT", "price": 3.1716731316027684e-08, "amount": 30.0, "tx_hash": "2NoMsScE5tJu3gzvrSn1uarnVZ4DcgXVLsc7BEwJ1ndU7T69xPbgyk53YW4MVCEfrSvdoaCup6VyM9pnf6YR6pGc"}
|
||||
{"timestamp": "2025-04-03T21:56:49.686823", "action": "buy", "token_address": "Hx4wR3Z7tEjfVip8jHFFsF3d4LwNmXgjBRkNR9qSUarF", "symbol": "Pablo", "price": 5.067234074416521e-08, "amount": 19.734632055953472, "tx_hash": "52hQYXPjtP1Rr945UW5peef6qbxThUubyBbEbcnGCCJ146nxx6bZXj3mCpWtpqpmMe82oNkEFwMms26JqhAaJMXL"}
|
||||
{"timestamp": "2025-04-03T21:57:09.656288", "action": "sell", "token_address": "Hx4wR3Z7tEjfVip8jHFFsF3d4LwNmXgjBRkNR9qSUarF", "symbol": "Pablo", "price": 2.8113188162567704e-08, "amount": 19.734632, "tx_hash": "xzpCFN8PgtPvXyRfQjkhHkPQcdzTNb6JXtCsQixtsMQabiBt9yNMSb7JLxRCT295rbVZuuocRNxYe6AqAiRnE91"}
|
||||
{"timestamp": "2025-04-24T20:30:13.087092", "action": "buy", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 5e-06, "amount": 20, "tx_hash": "3JvdfCep45PUB6rCcH4dB2NuwvFP8n67SCUxqJMt4MuN5ekHYc6J27aCUfwNUK3hh5rSyKNYAWXya5vQAT2qQivB"}
|
||||
{"timestamp": "2025-04-24T20:30:32.759177", "action": "sell", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 3.805530050663904e-08, "amount": 20.0, "tx_hash": "5cveLfU7XhPNCPMCZfTXyugJpmAQNmi7zr81PSqs8DsP1T2swYFjJwaB5hNSf3kFPfRzgzd7QZBVaZLd5MqsJevB"}
|
||||
|
||||
Reference in New Issue
Block a user