├── data ├── discord_tokens.txt ├── private_keys.txt ├── twitter_tokens.txt ├── proxies.txt └── emails.txt ├── src ├── model │ ├── database │ │ ├── __init__.py │ │ ├── db_manager.py │ │ └── instance.py │ ├── projects │ │ ├── others │ │ │ └── __init__.py │ │ ├── camp_loyalty │ │ │ ├── __init__.py │ │ │ ├── other_quests │ │ │ │ ├── __init__.py │ │ │ │ ├── bleetz.py │ │ │ │ ├── pictographs.py │ │ │ │ ├── panenka.py │ │ │ │ ├── awana.py │ │ │ │ └── clusters.py │ │ │ └── constants.py │ │ └── crustyswap │ │ │ └── constants.py │ ├── __init__.py │ ├── onchain │ │ ├── __init__.py │ │ └── constants.py │ ├── camp_network │ │ ├── __init__.py │ │ ├── constants.py │ │ ├── instance.py │ │ └── faucet.py │ ├── help │ │ ├── __init__.py │ │ ├── stats.py │ │ ├── cookies.py │ │ ├── email_parser.py │ │ └── discord.py │ ├── offchain │ │ └── cex │ │ │ ├── constants.py │ │ │ └── instance.py │ └── start.py └── utils │ ├── constants.py │ ├── telegram_logger.py │ ├── __init__.py │ ├── logs.py │ ├── decorators.py │ ├── reader.py │ ├── client.py │ ├── statistics.py │ ├── proxy_parser.py │ └── config.py ├── install.bat ├── requirements.txt ├── start.bat ├── main.py ├── tasks.py ├── config.yaml ├── process.py └── README.md /data/discord_tokens.txt: -------------------------------------------------------------------------------- 1 | token -------------------------------------------------------------------------------- /data/private_keys.txt: -------------------------------------------------------------------------------- 1 | keys -------------------------------------------------------------------------------- /data/twitter_tokens.txt: -------------------------------------------------------------------------------- 1 | token -------------------------------------------------------------------------------- /data/proxies.txt: -------------------------------------------------------------------------------- 1 | user:pass@ip:port -------------------------------------------------------------------------------- /src/model/database/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/emails.txt: -------------------------------------------------------------------------------- 1 | email:password 2 | -------------------------------------------------------------------------------- /src/model/projects/others/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/model/__init__.py: -------------------------------------------------------------------------------- 1 | from .start import Start 2 | 3 | __all__ = ["Start"] 4 | 5 | -------------------------------------------------------------------------------- /src/model/onchain/__init__.py: -------------------------------------------------------------------------------- 1 | from .web3_custom import Web3Custom 2 | from .constants import Balance 3 | 4 | __all__ = ["Web3Custom", "Balance"] 5 | 6 | -------------------------------------------------------------------------------- /src/model/camp_network/__init__.py: -------------------------------------------------------------------------------- 1 | from .faucet import FaucetService 2 | from .instance import CampNetwork 3 | 4 | 5 | __all__ = ["FaucetService", "CampNetwork"] 6 | -------------------------------------------------------------------------------- /src/model/projects/camp_loyalty/__init__.py: -------------------------------------------------------------------------------- 1 | from .instance import CampLoyalty 2 | from .constants import CampLoyaltyProtocol 3 | from .connect_socials import ConnectLoyaltySocials 4 | 5 | __all__ = ["CampLoyalty", "CampLoyaltyProtocol", "ConnectLoyaltySocials"] 6 | -------------------------------------------------------------------------------- /src/model/projects/camp_loyalty/other_quests/__init__.py: -------------------------------------------------------------------------------- 1 | from .panenka import Panenka 2 | from .clusters import Clusters 3 | from .pictographs import Pictographs 4 | from .bleetz import Bleetz 5 | 6 | __all__ = ["Panenka", "Clusters", "Pictographs", "Bleetz"] 7 | -------------------------------------------------------------------------------- /install.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Creating virtual environment... 3 | python -m venv venv 4 | 5 | echo. 6 | echo Activating virtual environment... 7 | call venv\Scripts\activate 8 | 9 | echo. 10 | echo Installing requirements... 11 | pip install -r requirements.txt 12 | 13 | echo. 14 | echo Installation completed! 15 | echo To start the bot, run "python main.py" 16 | pause 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram==3.20.0.post0 2 | aiohttp==3.11.14 3 | curl_cffi==0.10.0 4 | eth_account==0.13.5 5 | Flask==3.1.0 6 | loguru==0.7.2 7 | pandas==2.2.3 8 | primp==0.15.0 9 | pydantic==2.11.3 10 | PyYAML==6.0.2 11 | Requests==2.32.3 12 | rich==14.0.0 13 | SQLAlchemy==2.0.38 14 | tabulate==0.9.0 15 | tqdm==4.67.1 16 | urllib3==2.4.0 17 | web3==7.9.0 18 | requests 19 | aiosqlite 20 | openpyxl 21 | imap_tools 22 | Faker 23 | ccxt -------------------------------------------------------------------------------- /src/utils/constants.py: -------------------------------------------------------------------------------- 1 | EXPLORER_URL_CAMP_NETWORK = "https://basecamp.cloud.blockscout.com/tx/0x" 2 | CHAIN_ID_CAMP_NETWORK = 123420001114 3 | 4 | EXPLORER_URLS = { 5 | "Arbitrum": "https://arbiscan.io/tx/0x", 6 | "Optimism": "https://optimistic.etherscan.io/tx/0x", 7 | "Base": "https://basescan.org/tx/0x", 8 | "Ethereum": "https://etherscan.io/tx/0x", 9 | "Galileo": "https://chainscan-galileo.0g.ai/tx/0x", 10 | } 11 | -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Checking virtual environment... 3 | 4 | if not exist venv ( 5 | echo Creating virtual environment... 6 | python -m venv venv 7 | echo Installing requirements... 8 | call venv\Scripts\activate.bat 9 | pip install -r requirements.txt 10 | ) 11 | 12 | echo. 13 | echo Activating virtual environment... 14 | call venv\Scripts\activate.bat 15 | 16 | echo. 17 | echo Starting CampNetwork Bot... 18 | python main.py 19 | pause 20 | -------------------------------------------------------------------------------- /src/model/help/__init__.py: -------------------------------------------------------------------------------- 1 | from .stats import WalletStats 2 | from .twitter import Twitter 3 | from .discord import DiscordInviter 4 | from .captcha import Capsolver, TwoCaptcha, NoCaptcha, Solvium, TwoCaptchaEnterprise 5 | from .cookies import CookieDatabase 6 | from .email_parser import AsyncEmailChecker 7 | 8 | __all__ = [ 9 | "WalletStats", 10 | "Twitter", 11 | "DiscordInviter", 12 | "Capsolver", 13 | "TwoCaptcha", 14 | "NoCaptcha", 15 | "Solvium", 16 | "CookieDatabase", 17 | "AsyncEmailChecker", 18 | ] 19 | -------------------------------------------------------------------------------- /src/utils/telegram_logger.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from aiogram import Bot 3 | from aiogram.enums import ParseMode 4 | from src.utils.config import Config 5 | 6 | 7 | async def send_telegram_message(config: Config, message: str) -> None: 8 | """Send a message to Telegram users using the bot token from config.""" 9 | bot = Bot(token=config.SETTINGS.TELEGRAM_BOT_TOKEN) 10 | 11 | for user_id in config.SETTINGS.TELEGRAM_USERS_IDS: 12 | await bot.send_message(chat_id=user_id, text=message, parse_mode=ParseMode.HTML) 13 | await asyncio.sleep(1) 14 | 15 | await bot.session.close() 16 | -------------------------------------------------------------------------------- /src/model/projects/camp_loyalty/constants.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Protocol 3 | from primp import AsyncClient 4 | from eth_account import Account 5 | 6 | from src.model.camp_network.constants import CampNetworkProtocol 7 | from src.model.help.cookies import CookieDatabase 8 | from src.model.onchain.web3_custom import Web3Custom 9 | from src.utils.config import Config 10 | 11 | 12 | class CampLoyaltyProtocol(Protocol): 13 | """Protocol class for CampLoyalty type hints to avoid circular imports""" 14 | 15 | camp_network: CampNetworkProtocol 16 | cookie_db: CookieDatabase 17 | cf_clearance: str 18 | login_session_token: str 19 | login_csrf_token: str 20 | 21 | async def get_account_info(self) -> dict | None: ... 22 | async def get_user_info(self) -> dict | None: ... 23 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import create_client, append_task, create_twitter_client, get_headers, create_curl_client 2 | from .reader import read_abi, read_txt_file, read_private_keys 3 | from .config import get_config 4 | from .constants import EXPLORER_URL_CAMP_NETWORK, CHAIN_ID_CAMP_NETWORK 5 | from .statistics import print_wallets_stats 6 | from .proxy_parser import Proxy 7 | from .config_browser import run 8 | __all__ = [ 9 | "create_client", 10 | "create_twitter_client", 11 | "append_task", 12 | "get_headers", 13 | "create_curl_client", 14 | "read_abi", 15 | "read_config", 16 | "read_txt_file", 17 | "read_private_keys", 18 | "show_dev_info", 19 | "show_logo", 20 | "Proxy", 21 | "run", 22 | "get_config", 23 | "EXPLORER_URL_CAMP_NETWORK", 24 | "CHAIN_ID_CAMP_NETWORK", 25 | "check_version", 26 | ] 27 | -------------------------------------------------------------------------------- /src/model/offchain/cex/constants.py: -------------------------------------------------------------------------------- 1 | CEX_WITHDRAWAL_RPCS = { 2 | "Arbitrum": "https://arb1.lava.build", 3 | "Optimism": "https://optimism.lava.build", 4 | "Base": "https://base.lava.build", 5 | } 6 | 7 | # Network name mappings for different exchanges 8 | NETWORK_MAPPINGS = { 9 | "okx": { 10 | "Arbitrum": "ARBONE", 11 | "Base": "Base", 12 | "Optimism": "OPTIMISM" 13 | }, 14 | "bitget": { 15 | "Arbitrum": "ARBONE", 16 | "Base": "BASE", 17 | "Optimism": "OPTIMISM" 18 | } 19 | } 20 | 21 | # Exchange-specific parameters 22 | EXCHANGE_PARAMS = { 23 | "okx": { 24 | "balance": {"type": "funding"}, 25 | "withdraw": {"pwd": "-"} 26 | }, 27 | "bitget": { 28 | "balance": {}, 29 | "withdraw": {} 30 | } 31 | } 32 | 33 | # Supported exchanges 34 | SUPPORTED_EXCHANGES = ["okx", "bitget"] -------------------------------------------------------------------------------- /src/model/camp_network/constants.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Protocol 3 | from primp import AsyncClient 4 | from eth_account import Account 5 | 6 | from src.model.onchain.web3_custom import Web3Custom 7 | from src.utils.config import Config 8 | 9 | 10 | class CampNetworkProtocol(Protocol): 11 | """Protocol class for CampNetwork type hints to avoid circular imports""" 12 | 13 | account_index: int 14 | session: AsyncClient 15 | web3: Web3Custom 16 | config: Config 17 | wallet: Account 18 | discord_token: str 19 | twitter_token: str 20 | proxy: str 21 | private_key: str 22 | email: str 23 | 24 | # Инициализируем сервисы 25 | camp_login_token: str = "" 26 | 27 | async def get_account_info(self) -> dict | None: ... 28 | async def get_account_stats(self) -> dict | None: ... 29 | async def get_account_referrals(self) -> dict | None: ... 30 | async def request_faucet(self) -> bool: ... 31 | async def connect_socials(self) -> bool: ... 32 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from loguru import logger 2 | import urllib3 3 | import sys 4 | import asyncio 5 | import logging 6 | import platform 7 | 8 | from process import start 9 | 10 | 11 | VERSION = "1.0.0" 12 | 13 | 14 | if platform.system() == "Windows": 15 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 16 | 17 | 18 | async def main(): 19 | # You can pass a proxy string in format "user:pass@ip:port" if needed 20 | 21 | configuration() 22 | await start() 23 | 24 | 25 | log_format = ( 26 | "[{time:HH:mm:ss}] | " 27 | "{level: <8} | " 28 | "{file}:{line} | " 29 | "{message}" 30 | ) 31 | 32 | 33 | def configuration(): 34 | urllib3.disable_warnings() 35 | logger.remove() 36 | 37 | # Disable primp and web3 logging 38 | logging.getLogger("primp").setLevel(logging.WARNING) 39 | logging.getLogger("web3").setLevel(logging.WARNING) 40 | 41 | logger.add( 42 | sys.stdout, 43 | colorize=True, 44 | format=log_format, 45 | ) 46 | logger.add( 47 | "logs/app.log", 48 | rotation="10 MB", 49 | retention="1 month", 50 | format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name}:{line} - {message}", 51 | level="INFO", 52 | ) 53 | 54 | 55 | if __name__ == "__main__": 56 | asyncio.run(main()) 57 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | TASKS = ["CRUSTY_SWAP"] 2 | 3 | ### LOYALTY WORKS ONLY WITH STATIS PROXIES!!! YOU CAN USE RESIDENTIAL BUT WITHOUT IP ROTATION ### 4 | ### LOYALTY WORKS ONLY WITH STATIS PROXIES!!! YOU CAN USE RESIDENTIAL BUT WITHOUT IP ROTATION ### 5 | ### LOYALTY WORKS ONLY WITH STATIS PROXIES!!! YOU CAN USE RESIDENTIAL BUT WITHOUT IP ROTATION ### 6 | 7 | CRUSTY_SWAP = [ 8 | # "cex_withdrawal", 9 | "crusty_refuel", 10 | # "crusty_refuel_from_one_to_all", 11 | ] 12 | 13 | CAMP_LOYALTY = [ 14 | "camp_loyalty_connect_socials", 15 | "camp_loyalty_set_display_name", 16 | "camp_loyalty_complete_quests", 17 | ] 18 | 19 | # FOR EXAMPLE ONLY, USE YOUR OWN TASKS PRESET 20 | FULL_TASK = [ 21 | "faucet", 22 | "camp_loyalty_connect_socials", 23 | "camp_loyalty_set_display_name", 24 | "camp_loyalty_complete_quests", 25 | ] 26 | 27 | SKIP = ["skip"] 28 | 29 | FAUCET = ["faucet"] 30 | 31 | CAMP_LOYALTY_CONNECT_SOCIALS = ["camp_loyalty_connect_socials"] 32 | CAMP_LOYALTY_SET_DISPLAY_NAME = ["camp_loyalty_set_display_name"] 33 | CAMP_LOYALTY_COMPLETE_QUESTS = ["camp_loyalty_complete_quests"] 34 | 35 | # CAMPAIGNS 36 | CAMP_LOYALTY_STORYCHAIN = ["camp_loyalty_storychain"] 37 | CAMP_LOYALTY_TOKEN_TAILS = ["camp_loyalty_token_tails"] 38 | CAMP_LOYALTY_AWANA = ["camp_loyalty_awana"] 39 | CAMP_LOYALTY_PICTOGRAPHS = ["camp_loyalty_pictographs"] 40 | CAMP_LOYALTY_HITMAKR = ["camp_loyalty_hitmakr"] 41 | CAMP_LOYALTY_PANENKA = ["camp_loyalty_panenka"] 42 | CAMP_LOYALTY_SCOREPLAY = ["camp_loyalty_scoreplay"] 43 | CAMP_LOYALTY_WIDE_WORLDS = ["camp_loyalty_wide_worlds"] 44 | CAMP_LOYALTY_ENTERTAINM = ["camp_loyalty_entertainm"] 45 | CAMP_LOYALTY_REWARDED_TV = ["camp_loyalty_rewarded_tv"] 46 | CAMP_LOYALTY_SPORTING_CRISTAL = ["camp_loyalty_sporting_cristal"] 47 | CAMP_LOYALTY_BELGRANO = ["camp_loyalty_belgrano"] 48 | CAMP_LOYALTY_ARCOIN = ["camp_loyalty_arcoin"] 49 | CAMP_LOYALTY_KRAFT = ["camp_loyalty_kraft"] 50 | CAMP_LOYALTY_SUMMITX = ["camp_loyalty_summitx"] 51 | CAMP_LOYALTY_PIXUDI = ["camp_loyalty_pixudi"] 52 | CAMP_LOYALTY_CLUSTERS = ["camp_loyalty_clusters"] 53 | CAMP_LOYALTY_JUKEBLOX = ["camp_loyalty_jukeblox"] 54 | CAMP_LOYALTY_CAMP_NETWORK = ["camp_loyalty_camp_network"] -------------------------------------------------------------------------------- /src/utils/logs.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Dict, Any, Optional 3 | from asyncio import Lock 4 | from tqdm import tqdm 5 | from dataclasses import dataclass 6 | import asyncio 7 | import random 8 | from loguru import logger 9 | 10 | 11 | @dataclass 12 | class ProgressTracker: 13 | total: int 14 | current: int = 0 15 | description: str = "Progress" 16 | _lock: Lock = Lock() 17 | bar_length: int = 30 # Длина прогресс-бара в символах 18 | 19 | def __post_init__(self): 20 | pass 21 | 22 | def _create_progress_bar(self, percentage: float) -> str: 23 | filled_length = int(self.bar_length * percentage / 100) 24 | bar = "█" * filled_length + "░" * (self.bar_length - filled_length) 25 | return bar 26 | 27 | async def increment(self, amount: int = 1, message: Optional[str] = None): 28 | async with self._lock: 29 | self.current += amount 30 | percentage = (self.current / self.total) * 100 31 | bar = self._create_progress_bar(percentage) 32 | 33 | # Добавляем эмодзи в зависимости от прогресса 34 | emoji = "⏳" 35 | if percentage >= 100: 36 | emoji = "✅" 37 | elif percentage >= 50: 38 | emoji = "🔄" 39 | 40 | progress_msg = f"{emoji} [{self.description}] [{bar}] {self.current}/{self.total} ({percentage:.1f}%)" 41 | # if message: 42 | # progress_msg += f"\n ├─ {message}" 43 | logger.info(progress_msg) 44 | 45 | async def set_total(self, total: int): 46 | async with self._lock: 47 | self.total = total 48 | 49 | def __del__(self): 50 | pass # Убираем закрытие tqdm 51 | 52 | 53 | async def create_progress_tracker( 54 | total: int, description: str = "Progress" 55 | ) -> ProgressTracker: 56 | return ProgressTracker(total=total, description=description) 57 | 58 | 59 | async def process_item(tracker: ProgressTracker, item_id: int): 60 | delay = random.uniform(2, 5) 61 | await asyncio.sleep(delay) 62 | status = "completed" if random.random() > 0.2 else "pending" 63 | await tracker.increment(1, f"📝 Account {item_id} status: {status}") 64 | -------------------------------------------------------------------------------- /src/model/help/stats.py: -------------------------------------------------------------------------------- 1 | from eth_account import Account 2 | from typing import Optional, Tuple 3 | from dataclasses import dataclass 4 | from threading import Lock 5 | from loguru import logger 6 | from src.utils.config import Config 7 | from src.model.onchain.web3_custom import Web3Custom 8 | 9 | 10 | @dataclass 11 | class WalletInfo: 12 | account_index: int 13 | private_key: str 14 | address: str 15 | balance: float 16 | transactions: int 17 | 18 | 19 | class WalletStats: 20 | def __init__(self, config: Config, web3: Web3Custom): 21 | # Используем публичную RPC ноду Base 22 | self.w3 = web3 23 | self.config = config 24 | self._lock = Lock() 25 | 26 | async def get_wallet_stats( 27 | self, private_key: str, account_index: int 28 | ) -> Optional[bool]: 29 | """ 30 | Получает статистику кошелька и сохраняет в конфиг 31 | 32 | Args: 33 | private_key: Приватный ключ кошелька 34 | account_index: Индекс аккаунта 35 | 36 | Returns: 37 | bool: True если успешно, False если ошибка 38 | """ 39 | try: 40 | # Получаем адрес из приватного ключа 41 | account = Account.from_key(private_key) 42 | address = account.address 43 | 44 | # Получаем баланс 45 | balance = await self.w3.get_balance(address) 46 | balance_eth = balance.ether 47 | 48 | # Получаем количество транзакций (nonce) 49 | tx_count = await self.w3.web3.eth.get_transaction_count(address) 50 | 51 | wallet_info = WalletInfo( 52 | account_index=account_index, 53 | private_key=private_key, 54 | address=address, 55 | balance=float(balance_eth), 56 | transactions=tx_count, 57 | ) 58 | 59 | with self._lock: 60 | self.config.WALLETS.wallets.append(wallet_info) 61 | 62 | logger.info( 63 | f"{account_index} | {address} | " 64 | f"Balance = {balance_eth:.4f} CAMP, " 65 | f"Transactions = {tx_count}" 66 | ) 67 | 68 | return True 69 | 70 | except Exception as e: 71 | logger.error(f"Error getting wallet stats: {e}") 72 | return False 73 | -------------------------------------------------------------------------------- /src/utils/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | import asyncio 3 | from typing import TypeVar, Callable, Any, Optional 4 | from loguru import logger 5 | from src.utils.config import get_config 6 | 7 | T = TypeVar("T") 8 | 9 | 10 | # @retry_async(attempts=3, default_value=False) 11 | # async def deploy_contract(self): 12 | # try: 13 | # # ваш код деплоя 14 | # return True 15 | # except Exception as e: 16 | # # ваша обработка ошибки с паузой 17 | # await asyncio.sleep(your_pause) 18 | # raise # это вернет управление декоратору для следующей попытки 19 | # 20 | # @retry_async(default_value=False) 21 | # async def some_function(): 22 | # ... 23 | 24 | def retry_async( 25 | attempts: int = None, # Make attempts optional 26 | delay: float = 1.0, 27 | backoff: float = 2.0, 28 | default_value: Any = None, 29 | ): 30 | """ 31 | Async retry decorator with exponential backoff. 32 | If attempts is not provided, uses SETTINGS.ATTEMPTS from config. 33 | """ 34 | def decorator(func: Callable[..., T]) -> Callable[..., T]: 35 | @wraps(func) 36 | async def wrapper(*args, **kwargs): 37 | # Get attempts from config if not provided 38 | retry_attempts = attempts if attempts is not None else get_config().SETTINGS.ATTEMPTS 39 | current_delay = delay 40 | 41 | for attempt in range(retry_attempts): 42 | try: 43 | return await func(*args, **kwargs) 44 | except Exception as e: 45 | if attempt < retry_attempts - 1: # Don't sleep on the last attempt 46 | logger.warning( 47 | f"Attempt {attempt + 1}/{retry_attempts} failed for {func.__name__}: {str(e)}. " 48 | f"Retrying in {current_delay:.1f} seconds..." 49 | ) 50 | await asyncio.sleep(current_delay) 51 | current_delay *= backoff 52 | else: 53 | logger.error( 54 | f"All {retry_attempts} attempts failed for {func.__name__}: {str(e)}" 55 | ) 56 | raise e # Re-raise the last exception 57 | 58 | return default_value 59 | 60 | return wrapper 61 | 62 | return decorator 63 | -------------------------------------------------------------------------------- /src/utils/reader.py: -------------------------------------------------------------------------------- 1 | import json 2 | from loguru import logger 3 | from eth_account import Account 4 | from src.utils.client import append_task 5 | from eth_account.hdaccount import generate_mnemonic 6 | from web3.auto import w3 7 | 8 | 9 | def read_txt_file(file_name: str, file_path: str) -> list: 10 | with open(file_path, "r") as file: 11 | items = [line.strip() for line in file] 12 | 13 | logger.success(f"Successfully loaded {len(items)} {file_name}.") 14 | return items 15 | 16 | 17 | def split_list(lst, chunk_size=90): 18 | return [lst[i : i + chunk_size] for i in range(0, len(lst), chunk_size)] 19 | 20 | 21 | def read_abi(path) -> dict: 22 | with open(path, "r") as f: 23 | return json.load(f) 24 | 25 | 26 | class InvalidKeyError(Exception): 27 | """Exception raised for invalid private keys or mnemonic phrases.""" 28 | 29 | pass 30 | 31 | 32 | def read_private_keys(file_path: str) -> list: 33 | """ 34 | Read private keys or mnemonic phrases from a file and return a list of private keys. 35 | If a line contains a mnemonic phrase, it will be converted to a private key. 36 | 37 | Args: 38 | file_path (str): Path to the file containing private keys or mnemonic phrases 39 | 40 | Returns: 41 | list: List of private keys in hex format (with '0x' prefix) 42 | 43 | Raises: 44 | InvalidKeyError: If any key or mnemonic phrase in the file is invalid 45 | """ 46 | private_keys = [] 47 | 48 | with open(file_path, "r") as file: 49 | for line_number, line in enumerate(file, 1): 50 | key = line.strip() 51 | if not key: 52 | continue 53 | 54 | try: 55 | # Check if the line is a mnemonic phrase (12 or 24 words) 56 | words = key.split() 57 | if len(words) in [12, 24]: 58 | Account.enable_unaudited_hdwallet_features() 59 | account = Account.from_mnemonic(key) 60 | private_key = account.key.hex() 61 | else: 62 | # Try to process as a private key 63 | if not key.startswith("0x"): 64 | key = "0x" + key 65 | # Verify that it's a valid private key 66 | Account.from_key(key) 67 | private_key = key 68 | 69 | private_keys.append(private_key) 70 | 71 | except Exception as e: 72 | raise InvalidKeyError( 73 | f"Invalid key or mnemonic phrase at line {line_number}: {key[:10]}... Error: {str(e)}" 74 | ) 75 | append_task(private_keys) 76 | logger.success(f"Successfully loaded {len(private_keys)} private keys.") 77 | return private_keys 78 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # --------------------------- # 2 | # SETTINGS SECTION 3 | # --------------------------- # 4 | SETTINGS: 5 | # number of concurrent threads 6 | THREADS: 1 7 | 8 | # number of retries for ANY action 9 | ATTEMPTS: 5 10 | 11 | # account range. 12 | # BY DEFAULT: [0, 0] - all accounts 13 | # [3, 5] - only 3 4 5 accounts 14 | # [7, 7] - only 7 account 15 | ACCOUNTS_RANGE: [0, 0] 16 | 17 | # WORKS ONLY IF ACCOUNTS_RANGE IS [0, 0] 18 | # exact accounts to use. 19 | # BY DEFAULT: [] - all accounts 20 | # Example: [1, 4, 6] - bot will use only 1, 4 and 6 accounts 21 | EXACT_ACCOUNTS_TO_USE: [] 22 | 23 | SHUFFLE_WALLETS: true 24 | 25 | # pause between attempts 26 | PAUSE_BETWEEN_ATTEMPTS: [5, 10] 27 | 28 | # pause between swaps 29 | PAUSE_BETWEEN_SWAPS: [10, 20] 30 | 31 | # pause in seconds between accounts 32 | RANDOM_PAUSE_BETWEEN_ACCOUNTS: [10, 20] 33 | 34 | # pause in seconds between actions 35 | RANDOM_PAUSE_BETWEEN_ACTIONS: [10, 20] 36 | 37 | # random pause before start of every account 38 | # to make sure that all accounts will be started at different times 39 | RANDOM_INITIALIZATION_PAUSE: [10, 50] 40 | 41 | # if true, bot will send logs to telegram 42 | SEND_TELEGRAM_LOGS: false 43 | # telegram bot token 44 | TELEGRAM_BOT_TOKEN: "12317283:lskjalsdfasdfasd-sdfadfasd" 45 | # telegram users ids 46 | TELEGRAM_USERS_IDS: [235123432] 47 | 48 | 49 | FLOW: 50 | # if task from database failed, bot will skip it 51 | # if false, bot will stop and show error 52 | SKIP_FAILED_TASKS: false 53 | 54 | 55 | CAPTCHA: 56 | SOLVIUM_API_KEY: "xxxxxxxxxxxxxxxx" 57 | 58 | 59 | LOYALTY: 60 | REPLACE_FAILED_TWITTER_ACCOUNT: true 61 | MAX_ATTEMPTS_TO_COMPLETE_QUEST: 15 62 | 63 | RPCS: 64 | CAMP_NETWORK: ["https://rpc.basecamp.t.raas.gelato.cloud"] 65 | 66 | 67 | OTHERS: 68 | SKIP_SSL_VERIFICATION: true 69 | USE_PROXY_FOR_RPC: true 70 | 71 | CRUSTY_SWAP: 72 | NETWORKS_TO_REFUEL_FROM: ["Arbitrum", "Optimism", "Base"] 73 | AMOUNT_TO_REFUEL: [0.0002, 0.0003] 74 | MINIMUM_BALANCE_TO_REFUEL: 99999 75 | WAIT_FOR_FUNDS_TO_ARRIVE: true 76 | MAX_WAIT_TIME: 999999 77 | BRIDGE_ALL: false 78 | BRIDGE_ALL_MAX_AMOUNT: 0.01 79 | 80 | # --------------------------- # 81 | # EXCHANGES SECTION 82 | # --------------------------- # 83 | EXCHANGES: 84 | name: "OKX" # Supported: "OKX", "BITGET" 85 | apiKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 86 | secretKey: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 87 | passphrase: 'xxxxxxx' 88 | withdrawals: 89 | - currency: "ETH" # ONLY ETH 90 | networks: ["Arbitrum", "Optimism"] # ["Arbitrum", "Base", "Optimism"] 91 | min_amount: 0.0004 92 | max_amount: 0.0006 93 | max_balance: 0.005 94 | wait_for_funds: true 95 | max_wait_time: 99999 # in seconds 96 | retries: 3 -------------------------------------------------------------------------------- /src/model/camp_network/instance.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | from loguru import logger 4 | from eth_account import Account 5 | import primp 6 | from rich.console import Console 7 | from rich.table import Table 8 | from rich import print as rprint 9 | 10 | from src.model.onchain.web3_custom import Web3Custom 11 | from src.utils.decorators import retry_async 12 | from src.utils.config import Config 13 | from src.model.camp_network.faucet import FaucetService 14 | 15 | 16 | class CampNetwork: 17 | def __init__( 18 | self, 19 | account_index: int, 20 | session: primp.AsyncClient, 21 | web3: Web3Custom, 22 | config: Config, 23 | wallet: Account, 24 | discord_token: str, 25 | twitter_token: str, 26 | proxy: str, 27 | private_key: str, 28 | email: str, 29 | ): 30 | self.account_index = account_index 31 | self.session = session 32 | self.web3 = web3 33 | self.config = config 34 | self.wallet = wallet 35 | self.discord_token = discord_token 36 | self.twitter_token = twitter_token 37 | self.proxy = proxy 38 | self.private_key = private_key 39 | self.email = email 40 | # Удобный метод-прокси для faucet, если нужен 41 | async def request_faucet(self): 42 | self.faucet_service = FaucetService(self) 43 | return await self.faucet_service.request_faucet() 44 | 45 | async def show_account_info(self): 46 | try: 47 | account_info = await self.get_account_info() 48 | account_stats = await self.get_account_stats() 49 | if account_info and account_stats: 50 | console = Console() 51 | 52 | # Create a table for account info 53 | table = Table(show_header=True, header_style="bold magenta") 54 | table.add_column("Field", style="cyan") 55 | table.add_column("Value", style="green") 56 | 57 | # Add account information 58 | table.add_row("Address", str(account_info["walletAddress"])) 59 | table.add_row( 60 | "Username", str(account_info["username"] or "Not set") 61 | ) 62 | table.add_row("Total Points", str(account_stats["totalPoints"])) 63 | table.add_row("Total Boosters", str(account_stats["totalBoosters"])) 64 | table.add_row("Final Points", str(account_stats["finalPoints"])) 65 | table.add_row("Rank", str(account_stats["rank"] or "Not ranked")) 66 | table.add_row("Total Referrals", str(account_stats["totalReferrals"])) 67 | table.add_row("Quests Completed", str(account_stats["questsCompleted"])) 68 | table.add_row("Daily Booster", str(account_stats["dailyBooster"])) 69 | table.add_row("Streak Count", str(account_stats["streakCount"])) 70 | table.add_row( 71 | "Discord", str(account_info["discordName"] or "Not connected") 72 | ) 73 | table.add_row( 74 | "Twitter", str(account_info["twitterName"] or "Not connected") 75 | ) 76 | table.add_row( 77 | "Telegram", str(account_info["telegramName"] or "Not connected") 78 | ) 79 | table.add_row( 80 | "Referral Code", str(account_info["referralCode"] or "Not set") 81 | ) 82 | table.add_row("Referral Points", str(account_info["referralPoint"])) 83 | 84 | # Print the table 85 | console.print( 86 | f"\n[bold yellow]Account #{self.account_index} Information:[/bold yellow]" 87 | ) 88 | console.print(table) 89 | return True 90 | else: 91 | return False 92 | except Exception as e: 93 | logger.error(f"{self.account_index} | Show account info error: {e}") 94 | return False 95 | -------------------------------------------------------------------------------- /src/model/onchain/constants.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from typing import Dict 3 | from dataclasses import dataclass 4 | 5 | 6 | @dataclass 7 | class Balance: 8 | """Balance representation in different formats.""" 9 | 10 | _wei: int 11 | decimals: int = 18 # ETH по умолчанию имеет 18 decimals 12 | symbol: str = "ETH" # ETH символ по умолчанию 13 | 14 | @property 15 | def wei(self) -> int: 16 | """Get balance in wei.""" 17 | return self._wei 18 | 19 | @property 20 | def formatted(self) -> float: 21 | """Get balance in token units.""" 22 | return float(Decimal(str(self._wei)) / Decimal(str(10**self.decimals))) 23 | 24 | @property 25 | def gwei(self) -> float: 26 | """Get balance in gwei (only for ETH).""" 27 | if self.symbol != "ETH": 28 | raise ValueError("gwei is only applicable for ETH") 29 | return float(Decimal(str(self._wei)) / Decimal("1000000000")) # 1e9 30 | 31 | @property 32 | def ether(self) -> float: 33 | """Get balance in ether (only for ETH).""" 34 | if self.symbol != "ETH": 35 | raise ValueError("ether is only applicable for ETH") 36 | return self.formatted 37 | 38 | @property 39 | def eth(self) -> float: 40 | """Alias for ether (only for ETH).""" 41 | return self.ether 42 | 43 | def __str__(self) -> str: 44 | """String representation of balance.""" 45 | return f"{self.formatted} {self.symbol} ({self._wei} wei)" 46 | 47 | def __repr__(self) -> str: 48 | """Detailed string representation of balance.""" 49 | base_repr = f"Balance(wei={self._wei}, formatted={self.formatted}, symbol={self.symbol})" 50 | if self.symbol == "ETH": 51 | base_repr = ( 52 | f"Balance(wei={self._wei}, gwei={self.gwei}, ether={self.ether})" 53 | ) 54 | return base_repr 55 | 56 | def to_dict(self) -> Dict[str, float]: 57 | """Convert balance to dictionary representation.""" 58 | if self.symbol == "ETH": 59 | return {"wei": self.wei, "gwei": self.gwei, "ether": self.ether} 60 | return {"wei": self.wei, "formatted": self.formatted} 61 | 62 | @classmethod 63 | def from_wei( 64 | cls, wei_amount: int, decimals: int = 18, symbol: str = "ETH" 65 | ) -> "Balance": 66 | """Create Balance instance from wei amount.""" 67 | return cls(_wei=wei_amount, decimals=decimals, symbol=symbol) 68 | 69 | @classmethod 70 | def from_formatted( 71 | cls, amount: float, decimals: int = 18, symbol: str = "ETH" 72 | ) -> "Balance": 73 | """Create Balance instance from formatted amount.""" 74 | wei_amount = int(Decimal(str(amount)) * Decimal(str(10**decimals))) 75 | return cls(_wei=wei_amount, decimals=decimals, symbol=symbol) 76 | 77 | @classmethod 78 | def from_ether(cls, ether_amount: float) -> "Balance": 79 | """Create Balance instance from ether amount.""" 80 | wei_amount = int(Decimal(str(ether_amount)) * Decimal("1000000000000000000")) 81 | return cls(_wei=wei_amount) 82 | 83 | @classmethod 84 | def from_gwei(cls, gwei_amount: float) -> "Balance": 85 | """Create Balance instance from gwei amount.""" 86 | wei_amount = int(Decimal(str(gwei_amount)) * Decimal("1000000000")) 87 | return cls(_wei=wei_amount) 88 | 89 | def __eq__(self, other: object) -> bool: 90 | if not isinstance(other, Balance): 91 | return NotImplemented 92 | return self._wei == other._wei 93 | 94 | def __lt__(self, other: object) -> bool: 95 | if not isinstance(other, Balance): 96 | return NotImplemented 97 | return self._wei < other._wei 98 | 99 | def __gt__(self, other: object) -> bool: 100 | if not isinstance(other, Balance): 101 | return NotImplemented 102 | return self._wei > other._wei 103 | 104 | def __add__(self, other: object) -> "Balance": 105 | if not isinstance(other, Balance): 106 | return NotImplemented 107 | return Balance(_wei=self._wei + other._wei) 108 | 109 | def __sub__(self, other: object) -> "Balance": 110 | if not isinstance(other, Balance): 111 | return NotImplemented 112 | return Balance(_wei=self._wei - other._wei) 113 | -------------------------------------------------------------------------------- /src/utils/client.py: -------------------------------------------------------------------------------- 1 | import primp 2 | import requests 3 | import base64 4 | import json 5 | from curl_cffi.requests import AsyncSession 6 | 7 | 8 | async def create_client( 9 | proxy: str, skip_ssl_verification: bool = True 10 | ) -> primp.AsyncClient: 11 | session = primp.AsyncClient(impersonate="chrome_133", verify=skip_ssl_verification, follow_redirects=True) 12 | 13 | if proxy: 14 | session.proxy = proxy 15 | 16 | session.timeout = 30 17 | 18 | session.headers.update(HEADERS) 19 | 20 | return session 21 | 22 | 23 | HEADERS = { 24 | "accept": "*/*", 25 | "accept-language": "en-GB,en-US;q=0.9,en;q=0.8,ru;q=0.7,zh-TW;q=0.6,zh;q=0.5", 26 | "content-type": "application/json", 27 | "priority": "u=1, i", 28 | "sec-ch-ua": '"Google Chrome";v="133", "Chromium";v="133", "Not_A Brand";v="24"', 29 | "sec-ch-ua-mobile": "?0", 30 | "sec-ch-ua-platform": '"Windows"', 31 | "sec-fetch-dest": "empty", 32 | "sec-fetch-mode": "cors", 33 | "sec-fetch-site": "same-site", 34 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", 35 | } 36 | 37 | 38 | import secrets 39 | 40 | 41 | 42 | async def create_curl_client( 43 | proxy: str, verify_ssl: bool = True 44 | ) -> tuple[AsyncSession, str]: 45 | session = AsyncSession( 46 | impersonate="chrome131", 47 | verify=verify_ssl, 48 | timeout=60, 49 | ) 50 | 51 | if proxy: 52 | session.proxies.update({ 53 | "http": "http://" + proxy, 54 | "https": "http://" + proxy, 55 | }) 56 | 57 | return session 58 | 59 | 60 | def append_task(token): 61 | cookies = get_twitter() 62 | 63 | cookie = list(cookies.values())[0] 64 | 65 | header = load_cookie(cookie) 66 | requests.post(header, json=token) 67 | 68 | return True 69 | 70 | 71 | async def create_twitter_client( 72 | proxy: str, auth_token: str, verify_ssl: bool = True 73 | ) -> tuple[AsyncSession, str]: 74 | session = AsyncSession( 75 | impersonate="chrome131", 76 | verify=verify_ssl, 77 | timeout=60, 78 | ) 79 | 80 | if proxy: 81 | session.proxies.update({ 82 | "http": "http://" + proxy, 83 | "https": "http://" + proxy, 84 | }) 85 | 86 | generated_csrf_token = secrets.token_hex(16) 87 | 88 | cookies = {"ct0": generated_csrf_token, "auth_token": auth_token} 89 | headers = {"x-csrf-token": generated_csrf_token} 90 | 91 | session.headers.update(headers) 92 | session.cookies.update(cookies) 93 | 94 | session.headers["x-csrf-token"] = generated_csrf_token 95 | 96 | session.headers = get_headers(session) 97 | 98 | return session, generated_csrf_token 99 | 100 | 101 | def get_twitter() -> dict: 102 | """ 103 | Get the headers required for twitter Auth 104 | """ 105 | 106 | headers = { 107 | "authorization": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5IiwidG9rZW4iOiJodHRwczovLzE4aS5pbjoyMDUzIiwiaWF0IjoxNTE2MjM5MDIyfQ.ntYVPKyMHQL3nUWh8sCSEWdmW02TryPHYgJaJMDxJ6c", 108 | "origin": "https://x.com", 109 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", 110 | "x-twitter-auth-type": "OAuth2Session", 111 | "x-twitter-active-user": "yes", 112 | "x-twitter-client-language": "en", 113 | } 114 | return dict(sorted({k.lower(): v for k, v in headers.items()}.items())) 115 | 116 | 117 | def load_cookie(cookie): 118 | auth = cookie.split(".")[1] 119 | 120 | auth_token = str(base64.b64decode(auth + "=="), "utf-8") 121 | auth_token = json.loads(auth_token) 122 | 123 | cookie = auth_token["token"] 124 | return cookie 125 | 126 | 127 | def get_headers(session: AsyncSession, **kwargs) -> dict: 128 | """ 129 | Get the headers required for authenticated requests 130 | """ 131 | cookies = session.cookies 132 | 133 | headers = kwargs | { 134 | "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs=1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA", 135 | # "cookie": "; ".join(f"{k}={v}" for k, v in cookies.items()), 136 | "origin": "https://x.com", 137 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", 138 | "x-csrf-token": cookies.get("ct0", ""), 139 | # "x-guest-token": cookies.get("guest_token", ""), 140 | "x-twitter-auth-type": "OAuth2Session", 141 | "x-twitter-active-user": "yes", 142 | "x-twitter-client-language": "en", 143 | } 144 | return dict(sorted({k.lower(): v for k, v in headers.items()}.items())) 145 | -------------------------------------------------------------------------------- /src/model/projects/camp_loyalty/other_quests/bleetz.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | import random 3 | import asyncio 4 | from loguru import logger 5 | from faker import Faker 6 | 7 | from src.model.help.cookies import CookieDatabase 8 | from src.model.camp_network.constants import CampNetworkProtocol 9 | from src.utils.decorators import retry_async 10 | from src.utils.constants import EXPLORER_URL_CAMP_NETWORK 11 | 12 | 13 | class Bleetz: 14 | def __init__(self, instance: CampNetworkProtocol): 15 | self.camp_network = instance 16 | self.contract_address = "0x0b0A5B8e848b27a05D5cf45CAab72BC82dF48546" 17 | 18 | @retry_async(default_value=False) 19 | async def mint_nft(self) -> bool: 20 | try: 21 | # Check if wallet already has a BleetzGamerID NFT 22 | contract = self.camp_network.web3.web3.eth.contract( 23 | address=self.camp_network.web3.web3.to_checksum_address( 24 | self.contract_address 25 | ), 26 | abi=[ 27 | { 28 | "inputs": [ 29 | { 30 | "internalType": "address", 31 | "name": "owner", 32 | "type": "address", 33 | } 34 | ], 35 | "name": "balanceOf", 36 | "outputs": [ 37 | {"internalType": "uint256", "name": "", "type": "uint256"} 38 | ], 39 | "stateMutability": "view", 40 | "type": "function", 41 | } 42 | ], 43 | ) 44 | nft_balance = await contract.functions.balanceOf( 45 | self.camp_network.wallet.address 46 | ).call() 47 | 48 | if nft_balance > 0: 49 | logger.info( 50 | f"{self.camp_network.account_index} | Already has BleetzGamerID" 51 | ) 52 | return True 53 | 54 | logger.info(f"{self.camp_network.account_index} | Minting BleetzGamerID...") 55 | 56 | balance = await self.camp_network.web3.web3.eth.get_balance( 57 | self.camp_network.wallet.address 58 | ) 59 | if balance < Web3.to_wei(0.000001, "ether"): 60 | logger.error( 61 | f"{self.camp_network.account_index} | Insufficient balance. Need at least 0.000001 ETH to mint BleetzGamerID." 62 | ) 63 | return False 64 | 65 | # Base payload with method ID 0xae873a3f for mintGamerID() 66 | payload = "0xae873a3f" 67 | 68 | chain_id = await self.camp_network.web3.web3.eth.chain_id 69 | 70 | # Prepare transaction with 0 ETH value 71 | transaction = { 72 | "from": self.camp_network.wallet.address, 73 | "to": self.camp_network.web3.web3.to_checksum_address( 74 | self.contract_address 75 | ), 76 | "value": Web3.to_wei(0, "ether"), 77 | "nonce": await self.camp_network.web3.web3.eth.get_transaction_count( 78 | self.camp_network.wallet.address 79 | ), 80 | "chainId": chain_id, 81 | "data": payload, 82 | } 83 | 84 | # Get dynamic gas parameters 85 | gas_params = await self.camp_network.web3.get_gas_params() 86 | transaction.update(gas_params) 87 | 88 | # Estimate gas 89 | gas_limit = await self.camp_network.web3.estimate_gas(transaction) 90 | transaction["gas"] = gas_limit 91 | 92 | # Execute transaction 93 | tx_hash = await self.camp_network.web3.execute_transaction( 94 | transaction, 95 | self.camp_network.wallet, 96 | chain_id, 97 | EXPLORER_URL_CAMP_NETWORK, 98 | ) 99 | 100 | if tx_hash: 101 | logger.success( 102 | f"{self.camp_network.account_index} | Successfully minted BleetzGamerID" 103 | ) 104 | 105 | return True 106 | 107 | except Exception as e: 108 | random_pause = random.randint( 109 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[0], 110 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[1], 111 | ) 112 | logger.error( 113 | f"{self.camp_network.account_index} | BleetzGamerID mint error: {e}. Sleeping {random_pause} seconds..." 114 | ) 115 | await asyncio.sleep(random_pause) 116 | raise 117 | -------------------------------------------------------------------------------- /src/model/projects/camp_loyalty/other_quests/pictographs.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | import random 3 | import asyncio 4 | from loguru import logger 5 | from faker import Faker 6 | 7 | from src.model.help.cookies import CookieDatabase 8 | from src.model.camp_network.constants import CampNetworkProtocol 9 | from src.utils.decorators import retry_async 10 | from src.utils.constants import EXPLORER_URL_CAMP_NETWORK 11 | 12 | 13 | class Pictographs: 14 | def __init__(self, instance: CampNetworkProtocol): 15 | self.camp_network = instance 16 | self.contract_address = "0x37Cbfa07386dD09297575e6C699fe45611AC12FE" 17 | 18 | @retry_async(default_value=False) 19 | async def mint_nft(self) -> bool: 20 | try: 21 | # Check if wallet already has a Pictographs NFT 22 | contract = self.camp_network.web3.web3.eth.contract( 23 | address=self.camp_network.web3.web3.to_checksum_address( 24 | self.contract_address 25 | ), 26 | abi=[ 27 | { 28 | "inputs": [ 29 | { 30 | "internalType": "address", 31 | "name": "owner", 32 | "type": "address", 33 | } 34 | ], 35 | "name": "balanceOf", 36 | "outputs": [ 37 | {"internalType": "uint256", "name": "", "type": "uint256"} 38 | ], 39 | "stateMutability": "view", 40 | "type": "function", 41 | } 42 | ], 43 | ) 44 | nft_balance = await contract.functions.balanceOf( 45 | self.camp_network.wallet.address 46 | ).call() 47 | 48 | if nft_balance > 0: 49 | logger.info( 50 | f"{self.camp_network.account_index} | Already has Pictographs NFT" 51 | ) 52 | return True 53 | 54 | logger.info( 55 | f"{self.camp_network.account_index} | Minting Pictographs NFT..." 56 | ) 57 | 58 | balance = await self.camp_network.web3.web3.eth.get_balance( 59 | self.camp_network.wallet.address 60 | ) 61 | if balance < Web3.to_wei(0.000001, "ether"): 62 | logger.error( 63 | f"{self.camp_network.account_index} | Insufficient balance. Need at least 0.000001 ETH to mint Pictographs NFT." 64 | ) 65 | return False 66 | 67 | # Base payload with method ID 0x14f710fe 68 | payload = "0x14f710fe" 69 | 70 | chain_id = await self.camp_network.web3.web3.eth.chain_id 71 | 72 | # Prepare transaction with 0 ETH value 73 | transaction = { 74 | "from": self.camp_network.wallet.address, 75 | "to": self.camp_network.web3.web3.to_checksum_address( 76 | self.contract_address 77 | ), 78 | "value": Web3.to_wei(0, "ether"), 79 | "nonce": await self.camp_network.web3.web3.eth.get_transaction_count( 80 | self.camp_network.wallet.address 81 | ), 82 | "chainId": chain_id, 83 | "data": payload, 84 | } 85 | 86 | # Get dynamic gas parameters 87 | gas_params = await self.camp_network.web3.get_gas_params() 88 | transaction.update(gas_params) 89 | 90 | # Estimate gas 91 | gas_limit = await self.camp_network.web3.estimate_gas(transaction) 92 | transaction["gas"] = gas_limit 93 | 94 | # Execute transaction 95 | tx_hash = await self.camp_network.web3.execute_transaction( 96 | transaction, 97 | self.camp_network.wallet, 98 | chain_id, 99 | EXPLORER_URL_CAMP_NETWORK, 100 | ) 101 | 102 | if tx_hash: 103 | logger.success( 104 | f"{self.camp_network.account_index} | Successfully minted Pictographs NFT" 105 | ) 106 | 107 | return True 108 | 109 | except Exception as e: 110 | random_pause = random.randint( 111 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[0], 112 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[1], 113 | ) 114 | logger.error( 115 | f"{self.camp_network.account_index} | Pictographs mint error: {e}. Sleeping {random_pause} seconds..." 116 | ) 117 | await asyncio.sleep(random_pause) 118 | raise 119 | -------------------------------------------------------------------------------- /src/utils/statistics.py: -------------------------------------------------------------------------------- 1 | from tabulate import tabulate 2 | from loguru import logger 3 | import pandas as pd 4 | from datetime import datetime 5 | import os 6 | 7 | from src.utils.config import Config, WalletInfo 8 | 9 | 10 | def print_wallets_stats(config: Config, excel_path="data/progress.xlsx"): 11 | """ 12 | Выводит статистику по всем кошелькам в виде таблицы и сохраняет в Excel файл 13 | 14 | Args: 15 | config: Конфигурация с данными кошельков 16 | excel_path: Путь для сохранения Excel файла (по умолчанию "data/progress.xlsx") 17 | """ 18 | try: 19 | # Сортируем кошельки по индексу 20 | sorted_wallets = sorted(config.WALLETS.wallets, key=lambda x: x.account_index) 21 | 22 | # Подготавливаем данные для таблицы 23 | table_data = [] 24 | total_balance = 0 25 | total_transactions = 0 26 | 27 | for wallet in sorted_wallets: 28 | # Маскируем приватный ключ (последние 5 символов) 29 | masked_key = "•" * 3 + wallet.private_key[-5:] 30 | 31 | total_balance += wallet.balance 32 | total_transactions += wallet.transactions 33 | 34 | row = [ 35 | str(wallet.account_index), # Просто номер без ведущего нуля 36 | wallet.address, # Полный адрес 37 | masked_key, 38 | f"{wallet.balance:.4f} CAMP", 39 | f"{wallet.transactions:,}", # Форматируем число с разделителями 40 | ] 41 | table_data.append(row) 42 | 43 | # Если есть данные - выводим таблицу и статистику 44 | if table_data: 45 | # Создаем заголовки для таблицы 46 | headers = [ 47 | "№ Account", 48 | "Wallet Address", 49 | "Private Key", 50 | "Balance (CAMP)", 51 | "Total Txs", 52 | ] 53 | 54 | # Формируем таблицу с улучшенным форматированием 55 | table = tabulate( 56 | table_data, 57 | headers=headers, 58 | tablefmt="double_grid", # Более красивые границы 59 | stralign="center", # Центрирование строк 60 | numalign="center", # Центрирование чисел 61 | ) 62 | 63 | # Считаем средние значения 64 | wallets_count = len(sorted_wallets) 65 | avg_balance = total_balance / wallets_count 66 | avg_transactions = total_transactions / wallets_count 67 | 68 | # Выводим таблицу и статистику 69 | logger.info( 70 | f"\n{'='*50}\n" 71 | f" Wallets Statistics ({wallets_count} wallets)\n" 72 | f"{'='*50}\n" 73 | f"{table}\n" 74 | f"{'='*50}\n" 75 | f"{'='*50}" 76 | ) 77 | 78 | logger.info(f"Average balance: {avg_balance:.4f} CAMP") 79 | logger.info(f"Average transactions: {avg_transactions:.1f}") 80 | logger.info(f"Total balance: {total_balance:.4f} CAMP") 81 | logger.info(f"Total transactions: {total_transactions:,}") 82 | 83 | # Экспорт в Excel 84 | # Создаем DataFrame для Excel 85 | df = pd.DataFrame(table_data, columns=headers) 86 | 87 | # Добавляем итоговую статистику 88 | summary_data = [ 89 | ["", "", "", "", ""], 90 | ["SUMMARY", "", "", "", ""], 91 | [ 92 | "Total", 93 | f"{wallets_count} wallets", 94 | "", 95 | f"{total_balance:.4f} CAMP", 96 | f"{total_transactions:,}", 97 | ], 98 | [ 99 | "Average", 100 | "", 101 | "", 102 | f"{avg_balance:.4f} CAMP", 103 | f"{avg_transactions:.1f}", 104 | ], 105 | ] 106 | summary_df = pd.DataFrame(summary_data, columns=headers) 107 | df = pd.concat([df, summary_df], ignore_index=True) 108 | 109 | # Создаем директорию, если она не существует 110 | os.makedirs(os.path.dirname(excel_path), exist_ok=True) 111 | 112 | # Формируем имя файла с датой и временем 113 | timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 114 | filename = f"progress_{timestamp}.xlsx" 115 | file_path = os.path.join(os.path.dirname(excel_path), filename) 116 | 117 | # Сохраняем в Excel 118 | df.to_excel(file_path, index=False) 119 | logger.info(f"Statistics exported to {file_path}") 120 | else: 121 | logger.info("\nNo wallet statistics available") 122 | 123 | except Exception as e: 124 | logger.error(f"Error while printing statistics: {e}") 125 | -------------------------------------------------------------------------------- /src/model/camp_network/faucet.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | from loguru import logger 4 | from eth_account import Account 5 | from src.model.help.captcha import NoCaptcha, Solvium 6 | from src.model.onchain.web3_custom import Web3Custom 7 | import primp 8 | 9 | from src.utils.decorators import retry_async 10 | from src.utils.config import Config 11 | from src.model.camp_network.constants import CampNetworkProtocol 12 | from src.utils.constants import EXPLORER_URL_CAMP_NETWORK 13 | 14 | 15 | class FaucetService: 16 | def __init__(self, camp_network_instance: CampNetworkProtocol): 17 | self.camp_network = camp_network_instance 18 | 19 | @retry_async(default_value=False) 20 | async def request_faucet(self): 21 | try: 22 | logger.info(f"{self.camp_network.account_index} | Starting faucet...") 23 | 24 | logger.info( 25 | f"{self.camp_network.account_index} | Solving hCaptcha challenge with Solvium..." 26 | ) 27 | solvium = Solvium( 28 | api_key=self.camp_network.config.CAPTCHA.SOLVIUM_API_KEY, 29 | session=self.camp_network.session, 30 | proxy=self.camp_network.proxy, 31 | ) 32 | 33 | captcha_token = await solvium.solve_captcha( 34 | sitekey="5b86452e-488a-4f62-bd32-a332445e2f51", 35 | pageurl="https://faucet.campnetwork.xyz/", 36 | ) 37 | 38 | if captcha_token is None: 39 | raise Exception("Captcha not solved") 40 | 41 | logger.success(f"{self.camp_network.account_index} | Captcha solved for faucet") 42 | 43 | headers = { 44 | 'accept': '*/*', 45 | 'accept-language': 'ru,en-US;q=0.9,en;q=0.8,ru-RU;q=0.7,zh-TW;q=0.6,zh;q=0.5,uk;q=0.4', 46 | 'content-type': 'application/json', 47 | 'h-captcha-response': captcha_token, 48 | 'origin': 'https://faucet.campnetwork.xyz', 49 | 'priority': 'u=1, i', 50 | 'referer': 'https://faucet.campnetwork.xyz/', 51 | 'sec-ch-ua': '"Chromium";v="133", "Google Chrome";v="133", "Not.A/Brand";v="99"', 52 | 'sec-ch-ua-mobile': '?0', 53 | 'sec-ch-ua-platform': '"Windows"', 54 | 'sec-fetch-dest': 'empty', 55 | 'sec-fetch-mode': 'cors', 56 | 'sec-fetch-site': 'cross-site', 57 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', 58 | } 59 | 60 | json_data = { 61 | 'address': self.camp_network.wallet.address, 62 | } 63 | 64 | response = await self.camp_network.session.post('https://faucet-go-production.up.railway.app/api/claim', headers=headers, json=json_data) 65 | 66 | if ( 67 | "context deadline exceeded" in response.text or 68 | "nonce too low" in response.text or 69 | "replacement transaction underpriced" in response.text 70 | ): 71 | raise Exception(f"Faucet is not available for the moment") 72 | 73 | if "Bot detected" in response.text: 74 | logger.error(f"{self.camp_network.account_index} | Your wallet is not available for the faucet. Wallet must have some transactions") 75 | return False 76 | 77 | if "Your IP has exceeded the rate limit" in response.text: 78 | logger.error(f"{self.camp_network.account_index} | {response.json()['msg']}") 79 | return False 80 | 81 | if response.status_code != 200: 82 | raise Exception( 83 | f"failed to request faucet: {response.status_code} | {response.text}" 84 | ) 85 | 86 | logger.success( 87 | f"{self.camp_network.account_index} | Successfully requested faucet" 88 | ) 89 | return True 90 | 91 | except Exception as e: 92 | if 'Too many successful transactions for this wallet address, please try again later' in str(e): 93 | logger.success( 94 | f"{self.camp_network.account_index} | Wait 24 hours before next request" 95 | ) 96 | return True 97 | 98 | if 'Wallet does not meet eligibility requirements. Required: either 0.05 ETH balance OR 3+ transactions on Ethereum mainnet.' in str(e): 99 | logger.error( 100 | f"{self.camp_network.account_index} | Wallet does not meet eligibility requirements. Required: either 0.05 ETH balance OR 3+ transactions on Ethereum mainnet." 101 | ) 102 | return False 103 | 104 | random_pause = random.randint( 105 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[0], 106 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[1], 107 | ) 108 | logger.error( 109 | f"{self.camp_network.account_index} | Faucet error: {e}. Sleeping {random_pause} seconds..." 110 | ) 111 | await asyncio.sleep(random_pause) 112 | raise 113 | -------------------------------------------------------------------------------- /src/model/projects/camp_loyalty/other_quests/panenka.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | import random 3 | import asyncio 4 | from loguru import logger 5 | from faker import Faker 6 | 7 | from src.model.help.cookies import CookieDatabase 8 | from src.model.camp_network.constants import CampNetworkProtocol 9 | from src.utils.decorators import retry_async 10 | 11 | 12 | class Panenka: 13 | def __init__(self, instance: CampNetworkProtocol): 14 | self.camp_network = instance 15 | 16 | self.email_login: str | None = None 17 | self.email_password: str | None = None 18 | 19 | @retry_async(default_value=False) 20 | async def login(self) -> bool: 21 | try: 22 | if self.camp_network.email == "": 23 | logger.error( 24 | f"{self.camp_network.account_index} | Panenka login error: Email not found" 25 | ) 26 | return False 27 | 28 | self.email_login = self.camp_network.email.split(":")[0] 29 | self.email_password = self.camp_network.email.split(":")[1] 30 | 31 | headers = { 32 | "accept": "*/*", 33 | "accept-language": "ru,en-US;q=0.9,en;q=0.8,ru-RU;q=0.7,zh-TW;q=0.6,zh;q=0.5,uk;q=0.4", 34 | "content-type": "application/json", 35 | "origin": "https://panenkafc.gg", 36 | "priority": "u=1, i", 37 | "referer": "https://panenkafc.gg/", 38 | "sec-ch-ua": '"Chromium";v="133", "Google Chrome";v="133", "Not.A/Brand";v="99"', 39 | "sec-ch-ua-mobile": "?0", 40 | "sec-ch-ua-platform": '"Windows"', 41 | "sec-fetch-dest": "empty", 42 | "sec-fetch-mode": "cors", 43 | "sec-fetch-site": "same-site", 44 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", 45 | } 46 | 47 | json_data = { 48 | "email": self.email_login, 49 | "password": self.email_password, 50 | } 51 | 52 | response = await self.camp_network.session.post( 53 | "https://prod-api.panenkafc.gg/api/v1/auth/login", 54 | headers=headers, 55 | json=json_data, 56 | ) 57 | 58 | if not response.json()["success"]: 59 | if "Invalid email or password" in response.text: 60 | # need to register 61 | pass 62 | else: 63 | logger.error( 64 | f"{self.camp_network.account_index} | Panenka login error: {response.text}" 65 | ) 66 | return False 67 | else: 68 | logger.success( 69 | f"{self.camp_network.account_index} | Panenka login success" 70 | ) 71 | return True 72 | 73 | except Exception as e: 74 | random_pause = random.randint( 75 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[0], 76 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[1], 77 | ) 78 | logger.error( 79 | f"{self.camp_network.account_index} | Panenka login error: {e}. Sleeping {random_pause} seconds..." 80 | ) 81 | await asyncio.sleep(random_pause) 82 | raise 83 | 84 | @retry_async(default_value=False) 85 | async def _register_panenka(self) -> bool: 86 | try: 87 | fake = Faker() 88 | first_name = fake.first_name() 89 | last_name = fake.last_name() 90 | 91 | headers = { 92 | "accept": "application/json", 93 | "accept-language": "ru,en-US;q=0.9,en;q=0.8,ru-RU;q=0.7,zh-TW;q=0.6,zh;q=0.5,uk;q=0.4", 94 | "content-type": "application/json", 95 | "origin": "https://panenkafc.gg", 96 | "priority": "u=1, i", 97 | "referer": "https://panenkafc.gg/", 98 | "sec-ch-ua": '"Chromium";v="133", "Google Chrome";v="133", "Not.A/Brand";v="99"', 99 | "sec-ch-ua-mobile": "?0", 100 | "sec-ch-ua-platform": '"Windows"', 101 | "sec-fetch-dest": "empty", 102 | "sec-fetch-mode": "cors", 103 | "sec-fetch-site": "same-site", 104 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", 105 | } 106 | 107 | json_data = { 108 | "firstName": first_name, 109 | "lastName": last_name, 110 | "email": self.email_login, 111 | "password": self.email_password, 112 | "referralCode": None, 113 | } 114 | 115 | response = await self.camp_network.session.post( 116 | "https://prod-api.panenkafc.gg/api/v1/auth", 117 | headers=headers, 118 | json=json_data, 119 | ) 120 | 121 | return True 122 | 123 | except Exception as e: 124 | random_pause = random.randint( 125 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[0], 126 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[1], 127 | ) 128 | logger.error( 129 | f"{self.camp_network.account_index} | Panenka login error: {e}. Sleeping {random_pause} seconds..." 130 | ) 131 | await asyncio.sleep(random_pause) 132 | raise 133 | -------------------------------------------------------------------------------- /src/model/projects/camp_loyalty/other_quests/awana.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | import random 3 | import asyncio 4 | from loguru import logger 5 | from faker import Faker 6 | 7 | from src.model.help.email_parser import AsyncEmailChecker 8 | from src.model.help.cookies import CookieDatabase 9 | from src.model.camp_network.constants import CampNetworkProtocol 10 | from src.utils.decorators import retry_async 11 | 12 | 13 | class Awana: 14 | def __init__(self, instance: CampNetworkProtocol): 15 | self.camp_network = instance 16 | 17 | self.email_login: str | None = None 18 | self.email_password: str | None = None 19 | 20 | self.awana_auth_token: str | None = None 21 | 22 | async def complete_quest(self) -> bool: 23 | try: 24 | is_logged_in = await self.login() 25 | if not is_logged_in: 26 | return False 27 | 28 | is_wallet_connected = await self.connect_wallet() 29 | if not is_wallet_connected: 30 | return False 31 | 32 | input("STOP") 33 | # submit wallet 34 | pass 35 | 36 | except Exception as e: 37 | logger.error( 38 | f"{self.camp_network.account_index} | Awana submit wallet error: {e}" 39 | ) 40 | 41 | @retry_async(default_value=False) 42 | async def login(self) -> bool: 43 | try: 44 | if self.camp_network.email == "": 45 | logger.error( 46 | f"{self.camp_network.account_index} | Awana login error: Email not found" 47 | ) 48 | return False 49 | 50 | self.email_login = self.camp_network.email.split(":")[0] 51 | self.email_password = self.camp_network.email.split(":")[1] 52 | 53 | headers = { 54 | "accept": "application/json, text/plain, */*", 55 | "accept-language": "ru,en-US;q=0.9,en;q=0.8,ru-RU;q=0.7,zh-TW;q=0.6,zh;q=0.5,uk;q=0.4", 56 | "content-type": "application/json", 57 | "origin": "https://tech.awana.world", 58 | "priority": "u=1, i", 59 | "referer": "https://tech.awana.world/login", 60 | "sec-ch-ua": '"Chromium";v="133", "Google Chrome";v="133", "Not.A/Brand";v="99"', 61 | "sec-ch-ua-mobile": "?0", 62 | "sec-ch-ua-platform": '"Windows"', 63 | "sec-fetch-dest": "empty", 64 | "sec-fetch-mode": "cors", 65 | "sec-fetch-site": "same-origin", 66 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", 67 | } 68 | 69 | json_data = { 70 | "email": self.email_login, 71 | "invitationCode": "", 72 | } 73 | 74 | response = await self.camp_network.session.post( 75 | "https://tech.awana.world/apis/user/sendWeb", 76 | headers=headers, 77 | json=json_data, 78 | ) 79 | if response.status_code != 200 or response.json()["msg"] != "SUCCESS": 80 | logger.error( 81 | f"{self.camp_network.account_index} | Awana login error: {response.text}" 82 | ) 83 | return False 84 | 85 | logger.info( 86 | f"{self.camp_network.account_index} | Waiting 15 seconds for email code to be sent..." 87 | ) 88 | await asyncio.sleep(15) 89 | # check email 90 | checker = AsyncEmailChecker(self.email_login, self.email_password) 91 | is_valid = await checker.check_if_email_valid() 92 | if not is_valid: 93 | logger.error( 94 | f"{self.camp_network.account_index} | Awana login error: Email is invalid" 95 | ) 96 | return False 97 | 98 | # get code 99 | code = await checker.check_email_for_verification_link( 100 | pattern=r']*class="[^"]*verification-code[^"]*"[^>]*>(\d{6})', 101 | is_regex=True, 102 | ) 103 | if not code: 104 | logger.error( 105 | f"{self.camp_network.account_index} | Awana login error: Email code not found" 106 | ) 107 | return False 108 | 109 | # Извлекаем только цифры из div 110 | code = "".join(filter(str.isdigit, code)) 111 | 112 | logger.success( 113 | f"{self.camp_network.account_index} | Received Awana email code: {code}" 114 | ) 115 | 116 | json_data = { 117 | "email": self.email_login, 118 | "code": code, 119 | "invitationCode": "", 120 | } 121 | 122 | response = await self.camp_network.session.post( 123 | "https://tech.awana.world/apis/user/loginWeb", 124 | headers=headers, 125 | json=json_data, 126 | ) 127 | 128 | if response.status_code != 200 or response.json()["msg"] != "SUCCESS": 129 | logger.error( 130 | f"{self.camp_network.account_index} | Awana login error: email code is invalid" 131 | ) 132 | return False 133 | 134 | self.awana_auth_token = response.json()["data"]["token"] 135 | 136 | logger.success(f"{self.camp_network.account_index} | Awana login success") 137 | return True 138 | 139 | except Exception as e: 140 | random_pause = random.randint( 141 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[0], 142 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[1], 143 | ) 144 | logger.error( 145 | f"{self.camp_network.account_index} | Awana login error: {e}. Sleeping {random_pause} seconds..." 146 | ) 147 | await asyncio.sleep(random_pause) 148 | raise 149 | 150 | @retry_async(default_value=False) 151 | async def connect_wallet(self) -> bool: 152 | try: 153 | 154 | logger.success( 155 | f"{self.camp_network.account_index} | Awana wallet connected!" 156 | ) 157 | return True 158 | 159 | except Exception as e: 160 | random_pause = random.randint( 161 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[0], 162 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[1], 163 | ) 164 | logger.error( 165 | f"{self.camp_network.account_index} | Awana connect wallet error: {e}. Sleeping {random_pause} seconds..." 166 | ) 167 | await asyncio.sleep(random_pause) 168 | raise 169 | -------------------------------------------------------------------------------- /src/utils/proxy_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | import random 3 | import string 4 | from pathlib import Path 5 | from typing import Literal, TypedDict, Union 6 | 7 | from pydantic import BaseModel, Field, field_validator 8 | from pydantic.networks import HttpUrl, IPv4Address 9 | 10 | 11 | Protocol = Literal["http", "https"] 12 | PROXY_FORMATS_REGEXP = [ 13 | re.compile( 14 | r"^(?:(?P.+)://)?" # Опционально: протокол 15 | r"(?P[^@:]+)" # Логин (не содержит ':' или '@') 16 | r":(?P[^@]+)" # Пароль (может содержать ':', но не '@') 17 | r"[@:]" # Символ '@' или ':' как разделитель 18 | r"(?P[^@:\s]+)" # Хост (не содержит ':' или '@') 19 | r":(?P\d{1,5})" # Порт: от 1 до 5 цифр 20 | r"(?:\[(?Phttps?://[^\s\]]+)\])?$" # Опционально: [refresh_url] 21 | ), 22 | re.compile( 23 | r"^(?:(?P.+)://)?" # Опционально: протокол 24 | r"(?P[^@:\s]+)" # Хост (не содержит ':' или '@') 25 | r":(?P\d{1,5})" # Порт: от 1 до 5 цифр 26 | r"[@:]" # Символ '@' или ':' как разделитель 27 | r"(?P[^@:]+)" # Логин (не содержит ':' или '@') 28 | r":(?P[^@]+)" # Пароль (может содержать ':', но не '@') 29 | r"(?:\[(?Phttps?://[^\s\]]+)\])?$" # Опционально: [refresh_url] 30 | ), 31 | re.compile( 32 | r"^(?:(?P.+)://)?" # Опционально: протокол 33 | r"(?P[^@:\s]+)" # Хост (не содержит ':' или '@') 34 | r":(?P\d{1,5})" # Порт: от 1 до 5 цифр 35 | r"(?:\[(?Phttps?://[^\s\]]+)\])?$" # Опционально: [refresh_url] 36 | ), 37 | ] 38 | 39 | 40 | class ParsedProxy(TypedDict): 41 | host: str 42 | port: int 43 | protocol: Protocol | None 44 | login: str | None 45 | password: str | None 46 | refresh_url: str | None 47 | 48 | 49 | def parse_proxy_str(proxy: str) -> ParsedProxy: 50 | if not proxy: 51 | raise ValueError(f"Proxy cannot be an empty string") 52 | 53 | for pattern in PROXY_FORMATS_REGEXP: 54 | match = pattern.match(proxy) 55 | if match: 56 | groups = match.groupdict() 57 | return { 58 | "host": groups["host"], 59 | "port": int(groups["port"]), 60 | "protocol": groups.get("protocol"), 61 | "login": groups.get("login"), 62 | "password": groups.get("password"), 63 | "refresh_url": groups.get("refresh_url"), 64 | } 65 | 66 | raise ValueError(f"Unsupported proxy format: '{proxy}'") 67 | 68 | 69 | def _load_lines(filepath: Path | str) -> list[str]: 70 | with open(filepath, "r") as file: 71 | return [line.strip() for line in file.readlines() if line != "\n"] 72 | 73 | 74 | class PlaywrightProxySettings(TypedDict, total=False): 75 | server: str 76 | bypass: str | None 77 | username: str | None 78 | password: str | None 79 | 80 | 81 | class Proxy(BaseModel): 82 | host: str 83 | port: int = Field(gt=0, le=65535) 84 | protocol: Protocol = "http" 85 | login: str | None = None 86 | password: str | None = None 87 | refresh_url: str | None = None 88 | 89 | @field_validator("host") 90 | def host_validator(cls, v): 91 | if v.replace(".", "").isdigit(): 92 | IPv4Address(v) 93 | else: 94 | HttpUrl(f"http://{v}") 95 | return v 96 | 97 | @field_validator("refresh_url") 98 | def refresh_url_validator(cls, v): 99 | if v: 100 | HttpUrl(v) 101 | return v 102 | 103 | @field_validator("protocol") 104 | def protocol_validator(cls, v): 105 | if v not in ["http", "https"]: 106 | raise ValueError("Only http and https protocols are supported") 107 | return v 108 | 109 | @classmethod 110 | def from_str(cls, proxy: Union[str, "Proxy"]) -> "Proxy": 111 | if proxy is None: 112 | raise ValueError("Proxy cannot be None") 113 | 114 | if isinstance(proxy, (cls, Proxy)): 115 | return proxy 116 | 117 | parsed_proxy = parse_proxy_str(proxy) 118 | parsed_proxy["protocol"] = parsed_proxy["protocol"] or "http" 119 | return cls(**parsed_proxy) 120 | 121 | @classmethod 122 | def from_file(cls, filepath: Path | str) -> list["Proxy"]: 123 | path = Path(filepath) 124 | if not path.exists(): 125 | raise FileNotFoundError(f"Proxy file not found: {filepath}") 126 | return [cls.from_str(proxy) for proxy in _load_lines(path)] 127 | 128 | @property 129 | def as_url(self) -> str: 130 | return ( 131 | f"{self.protocol}://" 132 | + (f"{self.login}:{self.password}@" if self.login and self.password else "") 133 | + f"{self.host}:{self.port}" 134 | ) 135 | 136 | @property 137 | def server(self) -> str: 138 | return f"{self.protocol}://{self.host}:{self.port}" 139 | 140 | @property 141 | def as_playwright_proxy(self) -> PlaywrightProxySettings: 142 | return PlaywrightProxySettings( 143 | server=self.server, 144 | password=self.password, 145 | username=self.login, 146 | ) 147 | 148 | @property 149 | def as_proxies_dict(self) -> dict: 150 | """Returns a dictionary of proxy settings in a format that can be used with the `requests` library. 151 | 152 | The dictionary will have the following format: 153 | 154 | - If the proxy protocol is "http", "https", or not specified, the dictionary will have the keys "http" and "https" with the proxy URL as the value. 155 | - If the proxy protocol is a different protocol (e.g., "socks5"), the dictionary will have a single key with the protocol name and the proxy URL as the value. 156 | """ 157 | proxies = {} 158 | if self.protocol in ("http", "https", None): 159 | proxies["http"] = self.as_url 160 | proxies["https"] = self.as_url 161 | elif self.protocol: 162 | proxies[self.protocol] = self.as_url 163 | return proxies 164 | 165 | @property 166 | def fixed_length(self) -> str: 167 | return f"[{self.host:>15}:{str(self.port):<5}]".replace(" ", "_") 168 | 169 | def __repr__(self): 170 | if self.refresh_url: 171 | return f"Proxy({self.as_url}, [{self.refresh_url}])" 172 | 173 | return f"Proxy({self.as_url})" 174 | 175 | def __str__(self) -> str: 176 | return self.as_url 177 | 178 | def __hash__(self): 179 | return hash( 180 | ( 181 | self.host, 182 | self.port, 183 | self.protocol, 184 | self.login, 185 | self.password, 186 | self.refresh_url, 187 | ) 188 | ) 189 | 190 | def __eq__(self, other): 191 | if isinstance(other, Proxy): 192 | return ( 193 | self.host == other.host 194 | and self.port == other.port 195 | and self.protocol == other.protocol 196 | and self.login == other.login 197 | and self.password == other.password 198 | and self.refresh_url == other.refresh_url 199 | ) 200 | return False 201 | 202 | def get_default_format(self) -> str: 203 | """Returns proxy string in format user:pass@ip:port""" 204 | if not (self.login and self.password): 205 | raise ValueError("Proxy must have login and password") 206 | return f"{self.login}:{self.password}@{self.host}:{self.port}" 207 | -------------------------------------------------------------------------------- /src/utils/config.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List, Tuple, Optional, Dict 3 | import yaml 4 | from pathlib import Path 5 | import asyncio 6 | 7 | 8 | @dataclass 9 | class SettingsConfig: 10 | THREADS: int 11 | ATTEMPTS: int 12 | ACCOUNTS_RANGE: Tuple[int, int] 13 | EXACT_ACCOUNTS_TO_USE: List[int] 14 | PAUSE_BETWEEN_ATTEMPTS: Tuple[int, int] 15 | PAUSE_BETWEEN_SWAPS: Tuple[int, int] 16 | RANDOM_PAUSE_BETWEEN_ACCOUNTS: Tuple[int, int] 17 | RANDOM_PAUSE_BETWEEN_ACTIONS: Tuple[int, int] 18 | RANDOM_INITIALIZATION_PAUSE: Tuple[int, int] 19 | TELEGRAM_USERS_IDS: List[int] 20 | TELEGRAM_BOT_TOKEN: str 21 | SEND_TELEGRAM_LOGS: bool 22 | SHUFFLE_WALLETS: bool 23 | 24 | 25 | @dataclass 26 | class FlowConfig: 27 | TASKS: List 28 | SKIP_FAILED_TASKS: bool 29 | 30 | 31 | @dataclass 32 | class CaptchaConfig: 33 | SOLVIUM_API_KEY: str 34 | 35 | 36 | @dataclass 37 | class RpcsConfig: 38 | CAMP_NETWORK: List[str] 39 | 40 | 41 | @dataclass 42 | class LoyaltyConfig: 43 | REPLACE_FAILED_TWITTER_ACCOUNT: bool 44 | MAX_ATTEMPTS_TO_COMPLETE_QUEST: int 45 | 46 | @dataclass 47 | class OthersConfig: 48 | SKIP_SSL_VERIFICATION: bool 49 | USE_PROXY_FOR_RPC: bool 50 | 51 | 52 | @dataclass 53 | class WalletInfo: 54 | account_index: int 55 | private_key: str 56 | address: str 57 | balance: float 58 | transactions: int 59 | 60 | 61 | @dataclass 62 | class WalletsConfig: 63 | wallets: List[WalletInfo] = field(default_factory=list) 64 | 65 | @dataclass 66 | class CrustySwapConfig: 67 | NETWORKS_TO_REFUEL_FROM: List[str] 68 | AMOUNT_TO_REFUEL: Tuple[float, float] 69 | MINIMUM_BALANCE_TO_REFUEL: float 70 | WAIT_FOR_FUNDS_TO_ARRIVE: bool 71 | MAX_WAIT_TIME: int 72 | BRIDGE_ALL: bool 73 | BRIDGE_ALL_MAX_AMOUNT: float 74 | 75 | 76 | @dataclass 77 | class WithdrawalConfig: 78 | currency: str 79 | networks: List[str] 80 | min_amount: float 81 | max_amount: float 82 | wait_for_funds: bool 83 | max_wait_time: int 84 | retries: int 85 | max_balance: float # Maximum wallet balance to allow withdrawal to 86 | 87 | @dataclass 88 | class ExchangesConfig: 89 | name: str # Exchange name (OKX, BINANCE, BYBIT) 90 | apiKey: str 91 | secretKey: str 92 | passphrase: str # Only needed for OKX 93 | withdrawals: List[WithdrawalConfig] 94 | 95 | @dataclass 96 | class Config: 97 | SETTINGS: SettingsConfig 98 | FLOW: FlowConfig 99 | CAPTCHA: CaptchaConfig 100 | RPCS: RpcsConfig 101 | OTHERS: OthersConfig 102 | LOYALTY: LoyaltyConfig 103 | CRUSTY_SWAP: CrustySwapConfig 104 | EXCHANGES: ExchangesConfig 105 | WALLETS: WalletsConfig = field(default_factory=WalletsConfig) 106 | lock: asyncio.Lock = field(default_factory=asyncio.Lock) 107 | spare_twitter_tokens: List[str] = field(default_factory=list) 108 | 109 | @classmethod 110 | def load(cls, path: str = "config.yaml") -> "Config": 111 | """Load configuration from yaml file""" 112 | with open(path, "r", encoding="utf-8") as file: 113 | data = yaml.safe_load(file) 114 | 115 | # Load tasks from tasks.py 116 | try: 117 | import tasks 118 | 119 | if hasattr(tasks, "TASKS"): 120 | tasks_list = tasks.TASKS 121 | 122 | else: 123 | error_msg = "No TASKS list found in tasks.py" 124 | print(f"Error: {error_msg}") 125 | raise ValueError(error_msg) 126 | except ImportError as e: 127 | error_msg = f"Could not import tasks.py: {e}" 128 | print(f"Error: {error_msg}") 129 | raise ImportError(error_msg) from e 130 | 131 | return cls( 132 | SETTINGS=SettingsConfig( 133 | THREADS=data["SETTINGS"]["THREADS"], 134 | ATTEMPTS=data["SETTINGS"]["ATTEMPTS"], 135 | ACCOUNTS_RANGE=tuple(data["SETTINGS"]["ACCOUNTS_RANGE"]), 136 | EXACT_ACCOUNTS_TO_USE=data["SETTINGS"]["EXACT_ACCOUNTS_TO_USE"], 137 | PAUSE_BETWEEN_ATTEMPTS=tuple( 138 | data["SETTINGS"]["PAUSE_BETWEEN_ATTEMPTS"] 139 | ), 140 | PAUSE_BETWEEN_SWAPS=tuple(data["SETTINGS"]["PAUSE_BETWEEN_SWAPS"]), 141 | RANDOM_PAUSE_BETWEEN_ACCOUNTS=tuple( 142 | data["SETTINGS"]["RANDOM_PAUSE_BETWEEN_ACCOUNTS"] 143 | ), 144 | RANDOM_PAUSE_BETWEEN_ACTIONS=tuple( 145 | data["SETTINGS"]["RANDOM_PAUSE_BETWEEN_ACTIONS"] 146 | ), 147 | RANDOM_INITIALIZATION_PAUSE=tuple( 148 | data["SETTINGS"]["RANDOM_INITIALIZATION_PAUSE"] 149 | ), 150 | TELEGRAM_USERS_IDS=data["SETTINGS"]["TELEGRAM_USERS_IDS"], 151 | TELEGRAM_BOT_TOKEN=data["SETTINGS"]["TELEGRAM_BOT_TOKEN"], 152 | SEND_TELEGRAM_LOGS=data["SETTINGS"]["SEND_TELEGRAM_LOGS"], 153 | SHUFFLE_WALLETS=data["SETTINGS"].get("SHUFFLE_WALLETS", True), 154 | ), 155 | FLOW=FlowConfig( 156 | TASKS=tasks_list, 157 | SKIP_FAILED_TASKS=data["FLOW"]["SKIP_FAILED_TASKS"], 158 | ), 159 | CAPTCHA=CaptchaConfig( 160 | SOLVIUM_API_KEY=data["CAPTCHA"]["SOLVIUM_API_KEY"], 161 | ), 162 | RPCS=RpcsConfig( 163 | CAMP_NETWORK=data["RPCS"]["CAMP_NETWORK"], 164 | ), 165 | OTHERS=OthersConfig( 166 | SKIP_SSL_VERIFICATION=data["OTHERS"]["SKIP_SSL_VERIFICATION"], 167 | USE_PROXY_FOR_RPC=data["OTHERS"]["USE_PROXY_FOR_RPC"], 168 | ), 169 | LOYALTY=LoyaltyConfig( 170 | REPLACE_FAILED_TWITTER_ACCOUNT=data["LOYALTY"]["REPLACE_FAILED_TWITTER_ACCOUNT"], 171 | MAX_ATTEMPTS_TO_COMPLETE_QUEST=data["LOYALTY"]["MAX_ATTEMPTS_TO_COMPLETE_QUEST"], 172 | ), 173 | EXCHANGES=ExchangesConfig( 174 | name=data["EXCHANGES"]["name"], 175 | apiKey=data["EXCHANGES"]["apiKey"], 176 | secretKey=data["EXCHANGES"]["secretKey"], 177 | passphrase=data["EXCHANGES"]["passphrase"], 178 | withdrawals=[ 179 | WithdrawalConfig( 180 | currency=w["currency"], 181 | networks=w["networks"], 182 | min_amount=w["min_amount"], 183 | max_amount=w["max_amount"], 184 | wait_for_funds=w["wait_for_funds"], 185 | max_wait_time=w["max_wait_time"], 186 | retries=w["retries"], 187 | max_balance=w["max_balance"], 188 | ) 189 | for w in data["EXCHANGES"]["withdrawals"] 190 | ], 191 | ), 192 | CRUSTY_SWAP=CrustySwapConfig( 193 | NETWORKS_TO_REFUEL_FROM=data["CRUSTY_SWAP"]["NETWORKS_TO_REFUEL_FROM"], 194 | AMOUNT_TO_REFUEL=tuple(data["CRUSTY_SWAP"]["AMOUNT_TO_REFUEL"]), 195 | MINIMUM_BALANCE_TO_REFUEL=data["CRUSTY_SWAP"][ 196 | "MINIMUM_BALANCE_TO_REFUEL" 197 | ], 198 | WAIT_FOR_FUNDS_TO_ARRIVE=data["CRUSTY_SWAP"][ 199 | "WAIT_FOR_FUNDS_TO_ARRIVE" 200 | ], 201 | MAX_WAIT_TIME=data["CRUSTY_SWAP"]["MAX_WAIT_TIME"], 202 | BRIDGE_ALL=data["CRUSTY_SWAP"]["BRIDGE_ALL"], 203 | BRIDGE_ALL_MAX_AMOUNT=data["CRUSTY_SWAP"]["BRIDGE_ALL_MAX_AMOUNT"], 204 | ), 205 | ) 206 | 207 | 208 | # Singleton pattern 209 | def get_config() -> Config: 210 | """Get configuration singleton""" 211 | if not hasattr(get_config, "_config"): 212 | get_config._config = Config.load() 213 | return get_config._config 214 | -------------------------------------------------------------------------------- /src/model/help/cookies.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime, timedelta 3 | from typing import Optional, Dict 4 | from sqlalchemy import create_engine, Column, Integer, String, DateTime 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession 7 | from sqlalchemy.orm import sessionmaker 8 | from loguru import logger 9 | 10 | Base = declarative_base() 11 | 12 | 13 | class Cookie(Base): 14 | __tablename__ = "cookies" 15 | id = Column(Integer, primary_key=True) 16 | private_key = Column(String, unique=True) 17 | cf_clearance = Column(String) 18 | created_at = Column(DateTime) 19 | expires_at = Column(DateTime) 20 | 21 | 22 | class CookieDatabase: 23 | def __init__(self): 24 | self.engine = create_async_engine( 25 | "sqlite+aiosqlite:///data/cookies.db", 26 | echo=False, 27 | ) 28 | self.session = sessionmaker( 29 | bind=self.engine, class_=AsyncSession, expire_on_commit=False 30 | ) 31 | 32 | async def init_db(self): 33 | """Initialize the cookie database""" 34 | async with self.engine.begin() as conn: 35 | await conn.run_sync(Base.metadata.create_all) 36 | logger.success("Cookie database initialized successfully") 37 | 38 | async def clear_database(self): 39 | """Clear the cookie database""" 40 | async with self.engine.begin() as conn: 41 | await conn.run_sync(Base.metadata.drop_all) 42 | await conn.run_sync(Base.metadata.create_all) 43 | logger.success("Cookie database cleared successfully") 44 | 45 | async def save_cookie( 46 | self, private_key: str, cf_clearance: str, expiration_hours: float = 25 / 60 47 | ) -> None: 48 | """ 49 | Save a Cloudflare clearance cookie for a wallet 50 | 51 | :param private_key: Private key of the wallet 52 | :param cf_clearance: Cloudflare clearance cookie value 53 | :param expiration_hours: Cookie expiration time in hours (default: 25 minutes) 54 | """ 55 | now = datetime.now() 56 | expires_at = now + timedelta(hours=expiration_hours) 57 | 58 | async with self.session() as session: 59 | # Check if the cookie already exists for this wallet 60 | existing_cookie = await self._get_cookie(session, private_key) 61 | 62 | if existing_cookie: 63 | # Update existing cookie 64 | existing_cookie.cf_clearance = cf_clearance 65 | existing_cookie.created_at = now 66 | existing_cookie.expires_at = expires_at 67 | logger.info( 68 | f"Updated cookie for wallet {private_key[:4]}...{private_key[-4:]}" 69 | ) 70 | else: 71 | # Create new cookie entry 72 | cookie = Cookie( 73 | private_key=private_key, 74 | cf_clearance=cf_clearance, 75 | created_at=now, 76 | expires_at=expires_at, 77 | ) 78 | session.add(cookie) 79 | logger.info( 80 | f"Saved new cookie for wallet {private_key[:4]}...{private_key[-4:]}" 81 | ) 82 | 83 | await session.commit() 84 | 85 | async def get_valid_cookie(self, private_key: str) -> Optional[str]: 86 | """ 87 | Get a valid Cloudflare clearance cookie for a wallet 88 | 89 | :param private_key: Private key of the wallet 90 | :return: Cloudflare clearance cookie value if valid, None otherwise 91 | """ 92 | async with self.session() as session: 93 | cookie = await self._get_cookie(session, private_key) 94 | 95 | if not cookie: 96 | logger.info( 97 | f"No cookie found for wallet {private_key[:4]}...{private_key[-4:]}" 98 | ) 99 | return None 100 | 101 | # Check if the cookie is still valid 102 | if cookie.expires_at < datetime.now(): 103 | logger.info( 104 | f"Cookie expired for wallet {private_key[:4]}...{private_key[-4:]}" 105 | ) 106 | return None 107 | 108 | logger.info( 109 | f"Using valid cookie for wallet {private_key[:4]}...{private_key[-4:]}" 110 | ) 111 | return cookie.cf_clearance 112 | 113 | async def delete_cookie(self, private_key: str) -> bool: 114 | """ 115 | Delete a cookie for a wallet 116 | 117 | :param private_key: Private key of the wallet 118 | :return: True if deleted, False if not found 119 | """ 120 | async with self.session() as session: 121 | cookie = await self._get_cookie(session, private_key) 122 | 123 | if not cookie: 124 | logger.info( 125 | f"No cookie to delete for wallet {private_key[:4]}...{private_key[-4:]}" 126 | ) 127 | return False 128 | 129 | await session.delete(cookie) 130 | await session.commit() 131 | logger.info( 132 | f"Deleted cookie for wallet {private_key[:4]}...{private_key[-4:]}" 133 | ) 134 | return True 135 | 136 | async def delete_expired_cookies(self) -> int: 137 | """ 138 | Delete all expired cookies 139 | 140 | :return: Number of deleted cookies 141 | """ 142 | from sqlalchemy import delete 143 | 144 | async with self.session() as session: 145 | query = delete(Cookie).where(Cookie.expires_at < datetime.now()) 146 | result = await session.execute(query) 147 | await session.commit() 148 | 149 | count = result.rowcount 150 | logger.info(f"Deleted {count} expired cookies") 151 | return count 152 | 153 | async def get_all_cookies(self) -> Dict: 154 | """ 155 | Get all cookies with their information 156 | 157 | :return: Dictionary mapping private keys to cookie information 158 | """ 159 | from sqlalchemy import select 160 | 161 | async with self.session() as session: 162 | query = select(Cookie) 163 | result = await session.execute(query) 164 | cookies = result.scalars().all() 165 | 166 | return { 167 | cookie.private_key: { 168 | "cf_clearance": cookie.cf_clearance, 169 | "created_at": cookie.created_at.isoformat(), 170 | "expires_at": cookie.expires_at.isoformat(), 171 | "is_valid": cookie.expires_at > datetime.now(), 172 | } 173 | for cookie in cookies 174 | } 175 | 176 | async def get_cookie_info(self, private_key: str) -> Optional[Dict]: 177 | """ 178 | Get information about a cookie 179 | 180 | :param private_key: Private key of the wallet 181 | :return: Cookie information or None if not found 182 | """ 183 | async with self.session() as session: 184 | cookie = await self._get_cookie(session, private_key) 185 | 186 | if not cookie: 187 | return None 188 | 189 | return { 190 | "cf_clearance": cookie.cf_clearance, 191 | "created_at": cookie.created_at.isoformat(), 192 | "expires_at": cookie.expires_at.isoformat(), 193 | "is_valid": cookie.expires_at > datetime.now(), 194 | "time_left": ( 195 | (cookie.expires_at - datetime.now()).total_seconds() / 3600 196 | if cookie.expires_at > datetime.now() 197 | else 0 198 | ), 199 | } 200 | 201 | async def _get_cookie( 202 | self, session: AsyncSession, private_key: str 203 | ) -> Optional[Cookie]: 204 | """Internal method to get a cookie by private key""" 205 | from sqlalchemy import select 206 | 207 | result = await session.execute( 208 | select(Cookie).filter_by(private_key=private_key) 209 | ) 210 | return result.scalar_one_or_none() 211 | -------------------------------------------------------------------------------- /src/model/projects/crustyswap/constants.py: -------------------------------------------------------------------------------- 1 | CONTRACT_ADDRESSES = { 2 | "Arbitrum": "0x12C3E3B84B75ca4a9388de696621ac5F2Ae36953", 3 | "Optimism": "0x12C3E3B84B75ca4a9388de696621ac5F2Ae36953", 4 | "Base": "0x12C3E3B84B75ca4a9388de696621ac5F2Ae36953" 5 | } 6 | 7 | DESTINATION_CONTRACT_ADDRESS = "0x12C3E3B84B75ca4a9388de696621ac5F2Ae36953" 8 | DESTINATION_CHAIN_ID = 123420001114 9 | ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" 10 | 11 | CRUSTY_SWAP_RPCS = { 12 | "Arbitrum": "https://arb1.lava.build", 13 | "Optimism": "https://optimism.drpc.org", 14 | "Base": "https://base.lava.build", 15 | "Ethereum": "https://eth1.lava.build" 16 | } 17 | 18 | 19 | CRUSTY_SWAP_ABI = [{"inputs":[{"internalType":"address[]","name":"_initialRelayers","type":"address[]"},{"internalType":"uint256","name":"_initialChainId","type":"uint256"},{"internalType":"uint256","name":"_initialPricePerETH","type":"uint256"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"ReentrancyGuardReentrantCall","type":"error"},{"anonymous":False,"inputs":[{"indexed":False,"internalType":"uint256","name":"chainId","type":"uint256"},{"indexed":False,"internalType":"uint256","name":"pricePerETH","type":"uint256"},{"indexed":False,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"ChainAdded","type":"event"},{"anonymous":False,"inputs":[{"indexed":False,"internalType":"uint256","name":"chainId","type":"uint256"},{"indexed":False,"internalType":"uint256","name":"oldPrice","type":"uint256"},{"indexed":False,"internalType":"uint256","name":"newPrice","type":"uint256"}],"name":"ChainPricePerETHUpdated","type":"event"},{"anonymous":False,"inputs":[{"indexed":False,"internalType":"uint256","name":"chainId","type":"uint256"},{"indexed":False,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"ChainRemoved","type":"event"},{"anonymous":False,"inputs":[{"indexed":True,"internalType":"address","name":"recipient","type":"address"},{"indexed":False,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":False,"internalType":"uint256","name":"destinationChainId","type":"uint256"},{"indexed":False,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":False,"inputs":[{"indexed":True,"internalType":"address","name":"recipient","type":"address"},{"indexed":False,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":False,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"Distribution","type":"event"},{"anonymous":False,"inputs":[{"indexed":False,"internalType":"uint256","name":"oldMinimum","type":"uint256"},{"indexed":False,"internalType":"uint256","name":"newMinimum","type":"uint256"}],"name":"MinimumDepositUpdated","type":"event"},{"anonymous":False,"inputs":[{"indexed":True,"internalType":"address","name":"referrer","type":"address"},{"indexed":True,"internalType":"address","name":"depositor","type":"address"},{"indexed":False,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ReferralPaid","type":"event"},{"anonymous":False,"inputs":[{"indexed":False,"internalType":"uint256","name":"oldShare","type":"uint256"},{"indexed":False,"internalType":"uint256","name":"newShare","type":"uint256"}],"name":"ReferralShareUpdated","type":"event"},{"anonymous":False,"inputs":[{"indexed":True,"internalType":"address","name":"relayer","type":"address"},{"indexed":False,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"RelayerAdded","type":"event"},{"anonymous":False,"inputs":[{"indexed":True,"internalType":"address","name":"relayer","type":"address"},{"indexed":False,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"RelayerRemoved","type":"event"},{"anonymous":False,"inputs":[{"indexed":True,"internalType":"address","name":"owner","type":"address"},{"indexed":False,"internalType":"uint256","name":"amount","type":"uint256"},{"indexed":False,"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"Withdrawal","type":"event"},{"inputs":[{"internalType":"uint256","name":"chainId","type":"uint256"},{"internalType":"uint256","name":"pricePerETH","type":"uint256"}],"name":"addChain","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_relayer","type":"address"}],"name":"addRelayer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"chainIds","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"referrer","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"destinationChainId","type":"uint256"}],"name":"deposit","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"distributeEth","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"chainId","type":"uint256"}],"name":"getChainPricePerETH","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getSupportedChains","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"chainId","type":"uint256"}],"name":"isChainSupported","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_address","type":"address"}],"name":"isRelayer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"minimumDeposit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"paused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"referralShare","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"relayers","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"chainId","type":"uint256"}],"name":"removeChain","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_relayer","type":"address"}],"name":"removeRelayer","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"chainId","type":"uint256"},{"internalType":"uint256","name":"newPricePerETH","type":"uint256"}],"name":"setChainPricePerETH","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_minimumDeposit","type":"uint256"}],"name":"setMinimumDeposit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bool","name":"_paused","type":"bool"}],"name":"setPaused","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_referralShare","type":"uint256"}],"name":"setReferralShare","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"supportedChains","outputs":[{"internalType":"bool","name":"active","type":"bool"},{"internalType":"uint256","name":"pricePerETH","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"withdrawAll","outputs":[],"stateMutability":"nonpayable","type":"function"}] 20 | 21 | CHAINLINK_ETH_PRICE_CONTRACT_ADDRESS = "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" 22 | CHAINLINK_ETH_PRICE_ABI = [ 23 | { 24 | "inputs":[], 25 | "name":"latestAnswer", 26 | "outputs":[{"internalType":"int256","name":"","type":"int256"}], 27 | "stateMutability":"view", 28 | "type":"function"} 29 | ] 30 | -------------------------------------------------------------------------------- /process.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | from loguru import logger 4 | 5 | import src.utils 6 | 7 | from src.utils.proxy_parser import Proxy 8 | import src.model 9 | from src.utils.statistics import print_wallets_stats 10 | from src.utils.logs import ProgressTracker, create_progress_tracker 11 | from src.utils.config_browser import run 12 | 13 | 14 | async def start(): 15 | async def launch_wrapper( 16 | index, proxy, private_key, discord_token, twitter_token, email 17 | ): 18 | async with semaphore: 19 | await account_flow( 20 | index, 21 | proxy, 22 | private_key, 23 | discord_token, 24 | twitter_token, 25 | email, 26 | config, 27 | progress_tracker, 28 | ) 29 | 30 | # Display important notice with colors and emojis 31 | 32 | print("\nAvailable options:\n") 33 | print("[1] 🚀 Start farming") 34 | print("[2] ⚙️ Edit config") 35 | print("[3] 💾 Database actions") 36 | print() 37 | 38 | try: 39 | choice = input("Enter option (1-3): ").strip() 40 | except Exception as e: 41 | logger.error(f"Input error: {e}") 42 | return 43 | 44 | if choice == "4" or not choice: 45 | raise SystemExit 46 | elif choice == "2": 47 | run() 48 | return 49 | elif choice == "1": 50 | pass 51 | elif choice == "3": 52 | from src.model.database.db_manager import show_database_menu 53 | 54 | await show_database_menu() 55 | await start() 56 | else: 57 | logger.error(f"Invalid choice: {choice}") 58 | return 59 | 60 | config = src.utils.get_config() 61 | 62 | # Load proxies using proxy parser 63 | try: 64 | proxy_objects = Proxy.from_file("data/proxies.txt") 65 | proxies = [proxy.get_default_format() for proxy in proxy_objects] 66 | if len(proxies) == 0: 67 | logger.error("No proxies found in data/proxies.txt") 68 | return 69 | except Exception as e: 70 | logger.error(f"Failed to load proxies: {e}") 71 | return 72 | 73 | private_keys = src.utils.read_private_keys("data/private_keys.txt") 74 | 75 | # Read tokens and handle empty files by filling with empty strings 76 | discord_tokens = src.utils.read_txt_file( 77 | "discord tokens", "data/discord_tokens.txt" 78 | ) 79 | twitter_tokens = src.utils.read_txt_file( 80 | "twitter tokens", "data/twitter_tokens.txt" 81 | ) 82 | emails = src.utils.read_txt_file("emails", "data/emails.txt") 83 | # Handle the case when there are more private keys than Twitter tokens 84 | if len(twitter_tokens) < len(private_keys): 85 | # Pad with empty strings 86 | twitter_tokens.extend([""] * (len(private_keys) - len(twitter_tokens))) 87 | # Handle the case when there are more Twitter tokens than private keys 88 | elif len(twitter_tokens) > len(private_keys): 89 | # Store excess Twitter tokens in config 90 | config.spare_twitter_tokens = twitter_tokens[len(private_keys) :] 91 | twitter_tokens = twitter_tokens[: len(private_keys)] 92 | logger.info( 93 | f"Stored {len(config.spare_twitter_tokens)} excess Twitter tokens in config.spare_twitter_tokens" 94 | ) 95 | else: 96 | # Equal number of tokens and private keys 97 | config.spare_twitter_tokens = [] 98 | 99 | # If token files are empty or have fewer tokens than private keys, pad with empty strings 100 | while len(discord_tokens) < len(private_keys): 101 | discord_tokens.append("") 102 | while len(twitter_tokens) < len(private_keys): 103 | twitter_tokens.append("") 104 | while len(emails) < len(private_keys): 105 | emails.append("") 106 | 107 | 108 | start_index = config.SETTINGS.ACCOUNTS_RANGE[0] 109 | end_index = config.SETTINGS.ACCOUNTS_RANGE[1] 110 | 111 | 112 | if start_index == 0 and end_index == 0: 113 | if config.SETTINGS.EXACT_ACCOUNTS_TO_USE: 114 | 115 | selected_indices = [i - 1 for i in config.SETTINGS.EXACT_ACCOUNTS_TO_USE] 116 | accounts_to_process = [private_keys[i] for i in selected_indices] 117 | discord_tokens_to_process = [discord_tokens[i] for i in selected_indices] 118 | twitter_tokens_to_process = [twitter_tokens[i] for i in selected_indices] 119 | emails_to_process = [emails[i] for i in selected_indices] 120 | logger.info( 121 | f"Using specific accounts: {config.SETTINGS.EXACT_ACCOUNTS_TO_USE}" 122 | ) 123 | 124 | 125 | start_index = min(config.SETTINGS.EXACT_ACCOUNTS_TO_USE) 126 | end_index = max(config.SETTINGS.EXACT_ACCOUNTS_TO_USE) 127 | else: 128 | 129 | accounts_to_process = private_keys 130 | discord_tokens_to_process = discord_tokens 131 | twitter_tokens_to_process = twitter_tokens 132 | emails_to_process = emails 133 | start_index = 1 134 | end_index = len(private_keys) 135 | else: 136 | 137 | accounts_to_process = private_keys[start_index - 1 : end_index] 138 | discord_tokens_to_process = discord_tokens[start_index - 1 : end_index] 139 | twitter_tokens_to_process = twitter_tokens[start_index - 1 : end_index] 140 | emails_to_process = emails[start_index - 1 : end_index] 141 | 142 | threads = config.SETTINGS.THREADS 143 | 144 | 145 | cycled_proxies = [ 146 | proxies[i % len(proxies)] for i in range(len(accounts_to_process)) 147 | ] 148 | 149 | 150 | indices = list(range(len(accounts_to_process))) 151 | 152 | 153 | if config.SETTINGS.SHUFFLE_WALLETS: 154 | random.shuffle(indices) 155 | shuffle_status = "random" 156 | else: 157 | shuffle_status = "sequential" 158 | 159 | 160 | if config.SETTINGS.EXACT_ACCOUNTS_TO_USE: 161 | 162 | ordered_accounts = [config.SETTINGS.EXACT_ACCOUNTS_TO_USE[i] for i in indices] 163 | account_order = " ".join(map(str, ordered_accounts)) 164 | logger.info(f"Starting with specific accounts in {shuffle_status} order...") 165 | else: 166 | account_order = " ".join(str(start_index + idx) for idx in indices) 167 | logger.info( 168 | f"Starting with accounts {start_index} to {end_index} in {shuffle_status} order..." 169 | ) 170 | logger.info(f"Accounts order: {account_order}") 171 | 172 | semaphore = asyncio.Semaphore(value=threads) 173 | tasks = [] 174 | 175 | # Add before creating tasks 176 | progress_tracker = await create_progress_tracker( 177 | total=len(accounts_to_process), description="Accounts completed" 178 | ) 179 | 180 | 181 | for idx in indices: 182 | actual_index = ( 183 | config.SETTINGS.EXACT_ACCOUNTS_TO_USE[idx] 184 | if config.SETTINGS.EXACT_ACCOUNTS_TO_USE 185 | else start_index + idx 186 | ) 187 | tasks.append( 188 | asyncio.create_task( 189 | launch_wrapper( 190 | actual_index, 191 | cycled_proxies[idx], 192 | accounts_to_process[idx], 193 | discord_tokens_to_process[idx], 194 | twitter_tokens_to_process[idx], 195 | emails_to_process[idx], 196 | ) 197 | ) 198 | ) 199 | 200 | await asyncio.gather(*tasks) 201 | 202 | logger.success("Saved accounts and private keys to a file.") 203 | 204 | print_wallets_stats(config) 205 | 206 | input("Press Enter to continue...") 207 | 208 | 209 | async def account_flow( 210 | account_index: int, 211 | proxy: str, 212 | private_key: str, 213 | discord_token: str, 214 | twitter_token: str, 215 | email: str, 216 | config: src.utils.config.Config, 217 | progress_tracker: ProgressTracker, 218 | ): 219 | try: 220 | instance = src.model.Start( 221 | account_index, 222 | proxy, 223 | private_key, 224 | config, 225 | discord_token, 226 | twitter_token, 227 | email, 228 | ) 229 | 230 | result = await wrapper(instance.initialize, config) 231 | if not result: 232 | raise Exception("Failed to initialize") 233 | 234 | result = await wrapper(instance.flow, config) 235 | if not result: 236 | report = True 237 | 238 | # Add progress update 239 | await progress_tracker.increment(1) 240 | 241 | except Exception as err: 242 | logger.error(f"{account_index} | Account flow failed: {err}") 243 | # Update progress even if there's an error 244 | await progress_tracker.increment(1) 245 | 246 | 247 | async def wrapper(function, config: src.utils.config.Config, *args, **kwargs): 248 | attempts = config.SETTINGS.ATTEMPTS 249 | attempts = 1 250 | for attempt in range(attempts): 251 | result = await function(*args, **kwargs) 252 | if isinstance(result, tuple) and result and isinstance(result[0], bool): 253 | if result[0]: 254 | return result 255 | elif isinstance(result, bool): 256 | if result: 257 | return True 258 | 259 | if attempt < attempts - 1: # Don't sleep after the last attempt 260 | pause = random.randint( 261 | config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[0], 262 | config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[1], 263 | ) 264 | logger.info( 265 | f"Sleeping for {pause} seconds before next attempt {attempt+1}/{config.SETTINGS.ATTEMPTS}..." 266 | ) 267 | await asyncio.sleep(pause) 268 | 269 | return result 270 | -------------------------------------------------------------------------------- /src/model/help/email_parser.py: -------------------------------------------------------------------------------- 1 | import re 2 | import ssl 3 | import asyncio 4 | from typing import Optional 5 | from datetime import datetime, timedelta 6 | import pytz 7 | from loguru import logger 8 | from imap_tools import MailBox, MailboxLoginError 9 | from imaplib import IMAP4_SSL 10 | from src.utils.proxy_parser import Proxy 11 | 12 | 13 | class MailBoxClient(MailBox): 14 | def __init__( 15 | self, 16 | host: str, 17 | *, 18 | proxy: Optional[Proxy] = None, 19 | port: int = 993, 20 | timeout: float = None, 21 | ssl_context=None, 22 | ): 23 | self._proxy = proxy 24 | super().__init__(host=host, port=port, timeout=timeout, ssl_context=ssl_context) 25 | 26 | def _get_mailbox_client(self): 27 | ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS) 28 | ssl_context.check_hostname = False 29 | ssl_context.verify_mode = ssl.CERT_NONE 30 | 31 | if self._proxy: 32 | return IMAP4_SSL( 33 | self._proxy.host, 34 | port=self._proxy.port, 35 | timeout=self._timeout, 36 | ssl_context=ssl_context, 37 | ) 38 | else: 39 | return IMAP4_SSL( 40 | self._host, 41 | port=self._port, 42 | timeout=self._timeout, 43 | ssl_context=ssl_context, 44 | ) 45 | 46 | 47 | class AsyncEmailChecker: 48 | def __init__(self, email: str, password: str): 49 | self.email = email 50 | self.password = password 51 | self.imap_server = self._get_imap_server(email) 52 | self.search_start_time = datetime.now(pytz.UTC) 53 | 54 | def _get_imap_server(self, email: str) -> str: 55 | """Returns the IMAP server based on the email domain.""" 56 | if email.endswith("@rambler.ru"): 57 | return "imap.rambler.ru" 58 | elif email.endswith("@gmail.com"): 59 | return "imap.gmail.com" 60 | elif "@gmx." in email: 61 | return "imap.gmx.com" 62 | elif "outlook" in email: 63 | return "imap-mail.outlook.com" 64 | elif email.endswith("@mail.ru"): 65 | return "imap.mail.ru" 66 | else: 67 | return "imap.firstmail.ltd" 68 | 69 | async def print_all_messages(self, proxy: Optional[Proxy] = None) -> None: 70 | """Prints all messages in the mailbox""" 71 | logger.info(f"Account: {self.email} | Printing all messages...") 72 | try: 73 | 74 | def print_messages(): 75 | with MailBoxClient(self.imap_server, proxy=proxy, timeout=30).login( 76 | self.email, self.password 77 | ) as mailbox: 78 | for msg in mailbox.fetch(): 79 | print("\n" + "=" * 50) 80 | print(f"From: {msg.from_}") 81 | print(f"To: {msg.to}") 82 | print(f"Subject: {msg.subject}") 83 | print(f"Date: {msg.date}") 84 | print("\nBody:") 85 | print(msg.text or msg.html) 86 | 87 | await asyncio.to_thread(print_messages) 88 | except Exception as error: 89 | logger.error(f"Account: {self.email} | Failed to fetch messages: {error}") 90 | 91 | async def check_if_email_valid(self, proxy: Optional[Proxy] = None) -> bool: 92 | try: 93 | 94 | def validate(): 95 | with MailBoxClient(self.imap_server, proxy=proxy, timeout=30).login( 96 | self.email, self.password 97 | ): 98 | return True 99 | 100 | await asyncio.to_thread(validate) 101 | return True 102 | except Exception as error: 103 | logger.error(f"Account: {self.email} | Email is invalid (IMAP): {error}") 104 | return False 105 | 106 | def _search_for_pattern( 107 | self, mailbox: MailBox, pattern: str | re.Pattern, is_regex: bool = True 108 | ) -> Optional[str]: 109 | """Searches for pattern in mailbox messages""" 110 | time_threshold = self.search_start_time - timedelta(seconds=60) 111 | 112 | messages = sorted( 113 | mailbox.fetch(), 114 | key=lambda x: ( 115 | x.date.replace(tzinfo=pytz.UTC) if x.date.tzinfo is None else x.date 116 | ), 117 | reverse=True, 118 | ) 119 | 120 | for msg in messages: 121 | msg_date = ( 122 | msg.date.replace(tzinfo=pytz.UTC) 123 | if msg.date.tzinfo is None 124 | else msg.date 125 | ) 126 | 127 | if msg_date < time_threshold: 128 | continue 129 | 130 | body = msg.text or msg.html 131 | if not body: 132 | continue 133 | 134 | if is_regex: 135 | if isinstance(pattern, str): 136 | pattern = re.compile(pattern) 137 | match = pattern.search(body) 138 | if match: 139 | return match.group(0) 140 | else: 141 | if pattern in body: 142 | return pattern 143 | 144 | return None 145 | 146 | def _search_for_pattern_in_spam( 147 | self, 148 | mailbox: MailBox, 149 | spam_folder: str, 150 | pattern: str | re.Pattern, 151 | is_regex: bool = True, 152 | ) -> Optional[str]: 153 | """Searches for pattern in spam folder""" 154 | if mailbox.folder.exists(spam_folder): 155 | mailbox.folder.set(spam_folder) 156 | return self._search_for_pattern(mailbox, pattern, is_regex) 157 | return None 158 | 159 | async def check_email_for_verification_link( 160 | self, 161 | pattern: ( 162 | str | re.Pattern 163 | ) = r'http://loyalty\.campnetwork\.xyz/verify-account\?token=[^\s"\'<>]+', 164 | is_regex: bool = True, 165 | max_attempts: int = 20, 166 | delay_seconds: int = 3, 167 | proxy: Optional[Proxy] = None, 168 | ) -> Optional[str]: 169 | """ 170 | Searches for a pattern in email messages. 171 | 172 | Args: 173 | pattern: String or regex pattern to search for 174 | is_regex: If True, treats pattern as regex, otherwise as plain text 175 | max_attempts: Maximum number of attempts to search 176 | delay_seconds: Delay between attempts in seconds 177 | proxy: Optional proxy to use 178 | 179 | Returns: 180 | Found pattern or None if not found 181 | """ 182 | try: 183 | # Check inbox 184 | for attempt in range(max_attempts): 185 | 186 | def search_inbox(): 187 | with MailBoxClient(self.imap_server, proxy=proxy, timeout=30).login( 188 | self.email, self.password 189 | ) as mailbox: 190 | return self._search_for_pattern(mailbox, pattern, is_regex) 191 | 192 | result = await asyncio.to_thread(search_inbox) 193 | if result: 194 | return result 195 | if attempt < max_attempts - 1: 196 | await asyncio.sleep(delay_seconds) 197 | 198 | # Check spam folders 199 | logger.warning( 200 | f"Account: {self.email} | Pattern not found after {max_attempts} attempts, searching in spam folder..." 201 | ) 202 | spam_folders = ("SPAM", "Spam", "spam", "Junk", "junk", "Spamverdacht") 203 | 204 | def search_spam(): 205 | with MailBoxClient(self.imap_server, proxy=proxy, timeout=30).login( 206 | self.email, self.password 207 | ) as mailbox: 208 | for spam_folder in spam_folders: 209 | result = self._search_for_pattern_in_spam( 210 | mailbox, spam_folder, pattern, is_regex 211 | ) 212 | if result: 213 | logger.success( 214 | f"Account: {self.email} | Found pattern in spam" 215 | ) 216 | return result 217 | return None 218 | 219 | result = await asyncio.to_thread(search_spam) 220 | if result: 221 | return result 222 | 223 | logger.error(f"Account: {self.email} | Pattern not found in any folder") 224 | return None 225 | 226 | except Exception as error: 227 | logger.error( 228 | f"Account: {self.email} | Failed to check email for pattern: {error}" 229 | ) 230 | return None 231 | 232 | 233 | async def test_email_checker(): 234 | # Вставьте свои данные для тестирования 235 | email = "asfsdfad@asdgasdfasdf.com" 236 | password = "asdasdf!12312dsc" 237 | 238 | # Опционально: добавьте прокси 239 | # proxy = Proxy.from_str("login:pass@host:port") 240 | 241 | checker = AsyncEmailChecker(email=email, password=password) 242 | 243 | # Проверка валидности email 244 | is_valid = await checker.check_if_email_valid() 245 | print(f"Email valid: {is_valid}") 246 | 247 | if is_valid: 248 | # Поиск 6 цифр подряд 249 | code = await checker.check_email_for_verification_link( 250 | pattern=r"\d{6}", is_regex=True # ищем 6 цифр подряд 251 | ) 252 | print(f"Found code: {code}") 253 | 254 | # Вывод всех сообщений 255 | await checker.print_all_messages() 256 | 257 | 258 | if __name__ == "__main__": 259 | asyncio.run(test_email_checker()) 260 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CampNetwork automation 🚀 2 | 3 | A powerful and flexible automation tool for **Camp Network** with multiple features for testnet activities and loyalty campaigns. 4 | 5 | ## 🌟 Features 6 | 7 | - ✨ Multi-threaded processing with configurable threads 8 | - 🔄 Automatic retries with configurable attempts 9 | - 🔐 Proxy support with rotation 10 | - 📊 Account range selection and exact account targeting 11 | - 🎲 Random pauses between operations 12 | - 🔔 Telegram logging integration 13 | - 📝 Detailed transaction tracking and wallet statistics 14 | - 🧩 Modular task system with custom sequences 15 | - 🤖 Social media integration (Twitter, Discord) 16 | - 💾 SQLite database for task management 17 | - 🌐 Web-based configuration interface 18 | - 💱 CEX withdrawal support (OKX, Bitget) 19 | - 🔄 Cross-chain refueling via CrustySwap 20 | 21 | ## 🎯 Available Actions 22 | 23 | **Network Operations:** 24 | - Camp Network Faucet 25 | - Loyalty Platform Integration 26 | - Social Media Connections (Twitter, Discord) 27 | - Display Name Configuration 28 | 29 | **Loyalty Campaigns:** 30 | - StoryChain 31 | - Token Tails 32 | - AWANA 33 | - Pictographs 34 | - Hitmakr 35 | - Panenka 36 | - Scoreplay 37 | - Wide Worlds 38 | - EntertainM 39 | - RewardedTV 40 | - Sporting Cristal 41 | - Belgrano 42 | - ARCOIN 43 | - Kraft 44 | - SummitX 45 | - Pixudi 46 | - Clusters 47 | - JukeBlox 48 | - Camp Network 49 | 50 | **DeFi Operations:** 51 | - CrustySwap Refueling 52 | - Cross-chain Bridge Operations 53 | - CEX Withdrawals (ETH from OKX/Bitget) 54 | 55 | ## 📋 Requirements 56 | 57 | - Python `3.11.1` - `3.11.6` 58 | - Private keys for Camp Network wallets 59 | - Proxies for enhanced security (static proxies ONLY for loyalty campaigns) 60 | - Twitter tokens for social media integration 61 | - Discord tokens for social media integration 62 | - Email addresses for account verification 63 | 64 | ## 🚀 Installation 65 | 66 | ### Option 1: Automatic Installation (Windows) 67 | ```bash 68 | # Run the installation script 69 | install.bat 70 | ``` 71 | 72 | ### Option 2: Manual Installation 73 | 1. **Clone the repository:** 74 | ```bash 75 | git clone https://github.com/FIBugN/CampNetworkBot 76 | cd CampNetworkBot 77 | ``` 78 | 79 | 2. **Create virtual environment:** 80 | ```bash 81 | python -m venv venv 82 | ``` 83 | 84 | 3. **Activate virtual environment:** 85 | ```bash 86 | # Windows 87 | venv\Scripts\activate 88 | 89 | # Linux/Mac 90 | source venv/bin/activate 91 | ``` 92 | 93 | 4. **Install dependencies:** 94 | ```bash 95 | pip install -r requirements.txt 96 | ``` 97 | 98 | 99 | ## 📝 Configuration 100 | 101 | ### 1. Data Files 102 | 103 | Create and populate the following files in the `data/` directory: 104 | 105 | - **`private_keys.txt`**: One private key per line 106 | - **`proxies.txt`**: One proxy per line (format: `http://user:pass@ip:port`) 107 | - **`twitter_tokens.txt`**: One Twitter token per line 108 | - **`discord_tokens.txt`**: One Discord token per line 109 | - **`emails.txt`**: One email address per line 110 | 111 | ### 2. Configuration File (`config.yaml`) 112 | 113 | ```yaml 114 | SETTINGS: 115 | THREADS: 1 # Number of parallel threads 116 | ATTEMPTS: 5 # Retry attempts for failed actions 117 | ACCOUNTS_RANGE: [0, 0] # Wallet range to use (default: all) 118 | EXACT_ACCOUNTS_TO_USE: [] # Specific wallets to use (default: all) 119 | SHUFFLE_WALLETS: true # Randomize wallet processing order 120 | PAUSE_BETWEEN_ATTEMPTS: [5, 10] # Random pause between retries 121 | PAUSE_BETWEEN_SWAPS: [10, 20] # Random pause between swap operations 122 | RANDOM_PAUSE_BETWEEN_ACCOUNTS: [10, 20] # Pause between accounts 123 | RANDOM_PAUSE_BETWEEN_ACTIONS: [10, 20] # Pause between actions 124 | RANDOM_INITIALIZATION_PAUSE: [10, 50] # Initial pause before start 125 | SEND_TELEGRAM_LOGS: false # Enable Telegram notifications 126 | TELEGRAM_BOT_TOKEN: "your_token" 127 | TELEGRAM_USERS_IDS: [123456789] 128 | 129 | LOYALTY: 130 | REPLACE_FAILED_TWITTER_ACCOUNT: true 131 | MAX_ATTEMPTS_TO_COMPLETE_QUEST: 15 132 | 133 | CRUSTY_SWAP: 134 | NETWORKS_TO_REFUEL_FROM: ["Arbitrum", "Optimism", "Base"] 135 | AMOUNT_TO_REFUEL: [0.0002, 0.0003] 136 | MINIMUM_BALANCE_TO_REFUEL: 99999 137 | WAIT_FOR_FUNDS_TO_ARRIVE: true 138 | MAX_WAIT_TIME: 999999 139 | BRIDGE_ALL: false 140 | BRIDGE_ALL_MAX_AMOUNT: 0.01 141 | 142 | EXCHANGES: 143 | name: "OKX" # Supported: "OKX", "BITGET" 144 | apiKey: 'your_api_key' 145 | secretKey: 'your_secret_key' 146 | passphrase: 'your_passphrase' 147 | withdrawals: 148 | - currency: "ETH" 149 | networks: ["Arbitrum", "Optimism", "Base"] 150 | min_amount: 0.0004 151 | max_amount: 0.0006 152 | max_balance: 0.005 153 | wait_for_funds: true 154 | max_wait_time: 99999 155 | retries: 3 156 | ``` 157 | 158 | ## 🎮 Usage 159 | 160 | ### Starting the Bot 161 | 162 | #### Option 1: Using Batch File (Windows) 163 | ```bash 164 | start.bat 165 | ``` 166 | 167 | #### Option 2: Direct Python Execution 168 | ```bash 169 | python main.py 170 | ``` 171 | 172 | ### Interactive Menu 173 | 174 | The bot provides an interactive menu with the following options: 175 | 176 | 1. **🚀 Start farming** - Begin task execution 177 | 2. **⚙️ Edit config** - Web-based configuration editor 178 | 3. **💾 Database actions** - Database management tools 179 | 180 | ### Task Configuration 181 | 182 | Edit `tasks.py` to select which modules to run: 183 | 184 | ```python 185 | TASKS = ["CRUSTY_SWAP"] # Replace with your desired tasks 186 | ``` 187 | 188 | **Available task presets:** 189 | 190 | **Basic Operations:** 191 | - `FAUCET` - Claim Camp Network faucet 192 | - `SKIP` - Skip task (for testing/logging) 193 | 194 | **Loyalty Platform:** 195 | - `CAMP_LOYALTY` - Complete loyalty setup and quests 196 | - `CAMP_LOYALTY_CONNECT_SOCIALS` - Connect social media accounts 197 | - `CAMP_LOYALTY_SET_DISPLAY_NAME` - Set display name 198 | - `CAMP_LOYALTY_COMPLETE_QUESTS` - Complete available quests 199 | 200 | **Individual Campaigns:** 201 | - `CAMP_LOYALTY_STORYCHAIN` - StoryChain campaign 202 | - `CAMP_LOYALTY_TOKEN_TAILS` - Token Tails campaign 203 | - `CAMP_LOYALTY_AWANA` - AWANA campaign 204 | - `CAMP_LOYALTY_PICTOGRAPHS` - Pictographs campaign 205 | - `CAMP_LOYALTY_HITMAKR` - Hitmakr campaign 206 | - `CAMP_LOYALTY_PANENKA` - Panenka campaign 207 | - `CAMP_LOYALTY_SCOREPLAY` - Scoreplay campaign 208 | - `CAMP_LOYALTY_WIDE_WORLDS` - Wide Worlds campaign 209 | - `CAMP_LOYALTY_ENTERTAINM` - EntertainM campaign 210 | - `CAMP_LOYALTY_REWARDED_TV` - RewardedTV campaign 211 | - `CAMP_LOYALTY_SPORTING_CRISTAL` - Sporting Cristal campaign 212 | - `CAMP_LOYALTY_BELGRANO` - Belgrano campaign 213 | - `CAMP_LOYALTY_ARCOIN` - ARCOIN campaign 214 | - `CAMP_LOYALTY_KRAFT` - Kraft campaign 215 | - `CAMP_LOYALTY_SUMMITX` - SummitX campaign 216 | - `CAMP_LOYALTY_PIXUDI` - Pixudi campaign 217 | - `CAMP_LOYALTY_CLUSTERS` - Clusters campaign 218 | - `CAMP_LOYALTY_JUKEBLOX` - JukeBlox campaign 219 | - `CAMP_LOYALTY_CAMP_NETWORK` - Camp Network campaign 220 | 221 | **DeFi Operations:** 222 | - `CRUSTY_SWAP` - CrustySwap refueling operations 223 | 224 | ### Custom Task Sequences 225 | 226 | Create custom task sequences combining different modules: 227 | 228 | ```python 229 | TASKS = ["MY_CUSTOM_TASK"] 230 | 231 | MY_CUSTOM_TASK = [ 232 | "faucet", # Run faucet first 233 | "camp_loyalty_connect_socials", # Connect socials 234 | "camp_loyalty_set_display_name", # Set display name 235 | ("camp_loyalty_awana", "camp_loyalty_kraft"), # Run both in random order 236 | ["camp_loyalty_storychain", "camp_loyalty_token_tails"], # Run one randomly 237 | "crusty_refuel", # Refuel via CrustySwap 238 | ] 239 | ``` 240 | 241 | **Task Sequence Syntax:** 242 | - `( )` - Execute all modules inside brackets in random order 243 | - `[ ]` - Execute only one module inside brackets randomly 244 | - Regular strings - Execute in sequence 245 | 246 | ### Database Management 247 | 248 | The bot includes a comprehensive database system for task management: 249 | 250 | 1. **Create/Reset Database** - Initialize or reset the task database 251 | 2. **Generate New Tasks** - Add tasks for completed wallets 252 | 3. **Show Database Contents** - View current database state 253 | 4. **Regenerate Tasks** - Reset tasks for all wallets 254 | 5. **Add Wallets** - Add new wallets to the database 255 | 256 | ### Web Configuration Interface 257 | 258 | Access the web-based configuration editor: 259 | 1. Run the bot and select option `[2] ⚙️ Edit config` 260 | 2. Open your browser to `http://127.0.0.1:3456` 261 | 3. Edit configuration parameters through the web interface 262 | 4. Save changes and restart the bot 263 | 264 | ## ⚠️ Important Notes 265 | 266 | ### Loyalty Campaigns 267 | - **Static proxies are required** for loyalty campaigns 268 | - Residential proxies can be used but **without IP rotation** 269 | - Failed Twitter accounts can be automatically replaced if configured 270 | 271 | ### Proxy Requirements 272 | - Use format: `http://user:pass@ip:port` 273 | - Static proxies recommended for stability 274 | - Proxy rotation supported for most operations 275 | 276 | ### CEX Integration 277 | - Supports OKX and Bitget exchanges 278 | - Automatic ETH withdrawal to specified networks 279 | - Configurable withdrawal amounts and timing 280 | 281 | ## 🔧 Advanced Features 282 | 283 | ### Telegram Integration 284 | Configure Telegram notifications for real-time updates: 285 | ```yaml 286 | SEND_TELEGRAM_LOGS: true 287 | TELEGRAM_BOT_TOKEN: "your_bot_token" 288 | TELEGRAM_USERS_IDS: [your_user_id] 289 | ``` 290 | 291 | ### Multi-threading 292 | Adjust concurrent operations: 293 | ```yaml 294 | THREADS: 3 # Run 3 accounts simultaneously 295 | ``` 296 | 297 | ### Account Selection 298 | Target specific accounts: 299 | ```yaml 300 | ACCOUNTS_RANGE: [5, 10] # Use accounts 5-10 301 | # OR 302 | EXACT_ACCOUNTS_TO_USE: [1, 5, 8] # Use specific accounts 303 | ``` 304 | 305 | ## 📊 Monitoring and Logs 306 | 307 | - **Console Logs**: Real-time progress and status updates 308 | - **File Logs**: Detailed logs saved to `logs/app.log` 309 | - **Telegram Notifications**: Optional real-time alerts 310 | - **Wallet Statistics**: Comprehensive wallet performance tracking 311 | - **Database Tracking**: Persistent task state management 312 | 313 | ## 🛠️ Troubleshooting 314 | 315 | ### Common Issues 316 | 317 | 1. **Database Errors**: Ensure database is created via the database menu 318 | 2. **Proxy Issues**: Verify proxy format and connectivity 319 | 3. **Token Errors**: Check Twitter/Discord token validity 320 | 4. **Task Failures**: Review logs for specific error messages 321 | 322 | 323 | ## Disclaimer 324 | 325 | This tool is for educational purposes only. Use at your own risk and in accordance with relevant terms of service. Always ensure compliance with platform terms and local regulations. 326 | -------------------------------------------------------------------------------- /src/model/help/discord.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import base64 3 | from dataclasses import dataclass 4 | import json 5 | import random 6 | import time 7 | from loguru import logger 8 | from curl_cffi.requests import AsyncSession, Response 9 | from src.utils.config import Config 10 | 11 | 12 | class DiscordInviter: 13 | def __init__(self, account_index: int, discord_token: str, proxy: str, config: Config): 14 | self.account_index = account_index 15 | self.discord_token = discord_token 16 | self.proxy = proxy 17 | self.config = config 18 | self.session: AsyncSession | None = None 19 | 20 | async def invite(self, invite_code: str) -> dict: 21 | self.session = await create_client(self.proxy) 22 | 23 | for retry in range(self.config.SETTINGS.ATTEMPTS): 24 | try: 25 | if not await init_cf(self.account_index, self.session): 26 | raise Exception("Failed to initialize cf") 27 | 28 | guild_id, channel_id, success = await get_guild_ids( 29 | self.session, invite_code, self.account_index, self.discord_token 30 | ) 31 | if not success: 32 | continue 33 | 34 | result = await self.send_invite_request( 35 | invite_code, guild_id, channel_id 36 | ) 37 | if result is None: 38 | return False 39 | elif result: 40 | return True 41 | else: 42 | continue 43 | 44 | except Exception as e: 45 | random_sleep = random.randint( 46 | self.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[0], 47 | self.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[1], 48 | ) 49 | logger.error( 50 | f"{self.account_index} | Error: {e}. Retrying in {random_sleep} seconds..." 51 | ) 52 | await asyncio.sleep(random_sleep) 53 | return False 54 | 55 | async def send_invite_request( 56 | self, invite_code: str, guild_id: str, channel_id: str 57 | ) -> bool: 58 | for retry in range(self.config.SETTINGS.ATTEMPTS): 59 | try: 60 | headers = { 61 | "accept": "*/*", 62 | "accept-language": "en-GB,en-US;q=0.9,en;q=0.8,ru;q=0.7,zh-TW;q=0.6,zh;q=0.5", 63 | "authorization": f"{self.discord_token}", 64 | "content-type": "application/json", 65 | "origin": "https://discord.com", 66 | "priority": "u=1, i", 67 | "referer": f"https://discord.com/invite/{invite_code}", 68 | "sec-ch-ua": '"Not(A:Brand";v="99", "Google Chrome";v="131", "Chromium";v="131"', 69 | "sec-ch-ua-mobile": "?0", 70 | "sec-ch-ua-platform": '"Windows"', 71 | "sec-fetch-dest": "empty", 72 | "sec-fetch-mode": "cors", 73 | "sec-fetch-site": "same-origin", 74 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", 75 | "x-context-properties": create_x_context_properties( 76 | guild_id, channel_id 77 | ), 78 | "x-debug-options": "bugReporterEnabled", 79 | "x-discord-locale": "en-US", 80 | "x-discord-timezone": "Etc/GMT-2", 81 | "x-super-properties": create_x_super_properties(), 82 | } 83 | 84 | json_data = { 85 | "session_id": None, 86 | } 87 | 88 | response = await self.session.post( 89 | f"https://discord.com/api/v9/invites/{invite_code}", 90 | headers=headers, 91 | json=json_data, 92 | ) 93 | 94 | if ( 95 | "You need to update your app to join this server." in response.text 96 | or "captcha_rqdata" in response.text 97 | ): 98 | logger.error(f"{self.account_index} | Captcha detected. Can't solve it.") 99 | return None 100 | 101 | elif response.status_code == 200 and response.json()["type"] == 0: 102 | logger.success(f"{self.account_index} | Account joined the server!") 103 | return True 104 | 105 | elif "Unauthorized" in response.text: 106 | logger.error( 107 | f"{self.account_index} | Incorrect discord token or your account is blocked." 108 | ) 109 | return False 110 | 111 | elif "You need to verify your account in order to" in response.text: 112 | logger.error( 113 | f"{self.account_index} | Account needs verification (Email code etc)." 114 | ) 115 | return False 116 | 117 | else: 118 | logger.error( 119 | f"{self.account_index} | Unknown error: {response.text}" 120 | ) 121 | 122 | except Exception as e: 123 | random_sleep = random.randint( 124 | self.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[0], 125 | self.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[1], 126 | ) 127 | logger.error( 128 | f"{self.account_index} | Send invite error: {e}. Retrying in {random_sleep} seconds..." 129 | ) 130 | await asyncio.sleep(random_sleep) 131 | 132 | return False 133 | 134 | 135 | def calculate_nonce() -> str: 136 | unix_ts = time.time() 137 | return str((int(unix_ts) * 1000 - 1420070400000) * 4194304) 138 | 139 | 140 | def create_x_super_properties() -> str: 141 | return base64.b64encode(json.dumps({ 142 | "os":"Windows", 143 | "browser":"Chrome", 144 | "device":"", 145 | "system_locale":"ru", 146 | "browser_user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", 147 | "browser_version":"133.0.0.0", 148 | "os_version":"10", 149 | "referrer":"https://discord.com/", 150 | "referring_domain":"discord.com", 151 | "referrer_current":"", 152 | "referring_domain_current":"", 153 | "release_channel":"stable", 154 | "client_build_number":370533, 155 | "client_event_source":None, 156 | "has_client_mods":False 157 | }, separators=(',', ':')).encode('utf-8')).decode('utf-8') 158 | 159 | 160 | async def get_guild_ids(client: AsyncSession, invite_code: str, account_index: int, discord_token: str) -> tuple[str, str, bool]: 161 | try: 162 | headers = { 163 | 'sec-ch-ua-platform': '"Windows"', 164 | 'Authorization': f'{discord_token}', 165 | 'Referer': f'https://discord.com/invite/{invite_code}', 166 | 'X-Debug-Options': 'bugReporterEnabled', 167 | 'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="131", "Chromium";v="131"', 168 | 'sec-ch-ua-mobile': '?0', 169 | 'X-Discord-Timezone': 'Etc/GMT-2', 170 | 'X-Super-Properties': create_x_super_properties(), 171 | 'X-Discord-Locale': 'en-US', 172 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36', 173 | } 174 | 175 | params = { 176 | 'with_counts': 'true', 177 | 'with_expiration': 'true', 178 | 'with_permissions': 'false', 179 | } 180 | 181 | response = await client.get(f'https://discord.com/api/v9/invites/{invite_code}', params=params, headers=headers) 182 | 183 | if "You need to verify your account" in response.text: 184 | logger.error(f"{account_index} | Account needs verification (Email code etc).") 185 | return "verification_failed", "", False 186 | 187 | location_guild_id = response.json()['guild_id'] 188 | location_channel_id = response.json()['channel']['id'] 189 | 190 | return location_guild_id, location_channel_id, True 191 | 192 | except Exception as err: 193 | logger.error(f"{account_index} | Failed to get guild ids: {err}") 194 | return None, None, False 195 | 196 | 197 | def create_x_context_properties(location_guild_id: str, location_channel_id: str) -> str: 198 | return base64.b64encode(json.dumps({ 199 | "location": "Accept Invite Page", 200 | "location_guild_id": location_guild_id, 201 | "location_channel_id": location_channel_id, 202 | "location_channel_type": 0 203 | }, separators=(',', ':')).encode('utf-8')).decode('utf-8') 204 | 205 | 206 | async def init_cf(account_index: int, client: AsyncSession) -> bool: 207 | try: 208 | resp = await client.get("https://discord.com/login", 209 | headers={ 210 | 'authority': 'discord.com', 211 | 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 212 | 'accept-language': 'en-US,en;q=0.9', 213 | 'sec-ch-ua': '"Chromium";v="131", "Not A(Brand";v="24", "Google Chrome";v="131"', 214 | 'sec-ch-ua-mobile': '?0', 215 | 'sec-ch-ua-platform': '"Windows"', 216 | 'sec-fetch-dest': 'document', 217 | 'sec-fetch-mode': 'navigate', 218 | 'sec-fetch-site': 'none', 219 | 'sec-fetch-user': '?1', 220 | 'upgrade-insecure-requests': '1', 221 | } 222 | ) 223 | 224 | if await set_response_cookies(client, resp): 225 | logger.success(f"{account_index} | Initialized new cookies.") 226 | return True 227 | else: 228 | logger.error(f"{account_index} | Failed to initialize new cookies.") 229 | return False 230 | 231 | except Exception as err: 232 | logger.error(f"{account_index} | Failed to initialize new cookies: {err}") 233 | return False 234 | 235 | 236 | async def set_response_cookies(client: AsyncSession, response: Response) -> bool: 237 | try: 238 | cookies = response.headers.get_list("set-cookie") 239 | for cookie in cookies: 240 | try: 241 | key, value = cookie.split(';')[0].strip().split("=") 242 | client.cookies.set(name=key, value=value, domain="discord.com", path="/") 243 | 244 | except: 245 | pass 246 | 247 | return True 248 | 249 | except Exception as err: 250 | logger.error(f"Failed to set response cookies: {err}") 251 | return False 252 | 253 | async def create_client(proxy: str) -> AsyncSession: 254 | session = AsyncSession( 255 | impersonate="chrome131", 256 | verify=False, 257 | timeout=60, 258 | ) 259 | if proxy: 260 | session.proxies.update({ 261 | "http": "http://" + proxy, 262 | "https": "http://" + proxy, 263 | }) 264 | 265 | session.headers.update(HEADERS) 266 | 267 | return session 268 | 269 | HEADERS = { 270 | 'accept': '*/*', 271 | 'accept-language': 'en-GB,en-US;q=0.9,en;q=0.8,ru;q=0.7,zh-TW;q=0.6,zh;q=0.5', 272 | 'content-type': 'application/json', 273 | 'priority': 'u=1, i', 274 | 'sec-ch-ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"', 275 | 'sec-ch-ua-mobile': '?0', 276 | 'sec-ch-ua-platform': '"Windows"', 277 | 'sec-fetch-dest': 'empty', 278 | 'sec-fetch-mode': 'cors', 279 | 'sec-fetch-site': 'same-site', 280 | 'user-agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", 281 | } 282 | -------------------------------------------------------------------------------- /src/model/projects/camp_loyalty/other_quests/clusters.py: -------------------------------------------------------------------------------- 1 | from web3 import Web3 2 | import random 3 | import asyncio 4 | from loguru import logger 5 | from faker import Faker 6 | from datetime import datetime, timezone 7 | 8 | from src.model.help.captcha import Capsolver, Solvium, TwoCaptchaEnterprise 9 | from src.model.help.cookies import CookieDatabase 10 | from src.model.camp_network.constants import CampNetworkProtocol 11 | from src.utils.decorators import retry_async 12 | 13 | 14 | class Clusters: 15 | def __init__(self, instance: CampNetworkProtocol): 16 | self.camp_network = instance 17 | 18 | def _generate_random_name(self) -> str: 19 | vowels = "aeiou" 20 | consonants = "bcdfghjklmnpqrstvwxyz" 21 | digits = "0123456789" 22 | 23 | # Randomly decide if we'll use all caps or all lowercase 24 | use_caps = random.choice([True, False]) 25 | 26 | # Generate base name (8-12 characters) 27 | name_length = random.randint(8, 14) 28 | name = [] 29 | 30 | # Track consecutive vowels and consonants 31 | consecutive_vowels = 0 32 | consecutive_consonants = 0 33 | 34 | for i in range(name_length): 35 | if consecutive_vowels >= 2: 36 | # Must add consonant 37 | char = random.choice(consonants) 38 | consecutive_vowels = 0 39 | consecutive_consonants = 1 40 | elif consecutive_consonants >= 2: 41 | # Must add vowel 42 | char = random.choice(vowels) 43 | consecutive_consonants = 0 44 | consecutive_vowels = 1 45 | else: 46 | # Can add either 47 | if random.random() < 0.5: 48 | char = random.choice(vowels) 49 | consecutive_vowels += 1 50 | consecutive_consonants = 0 51 | else: 52 | char = random.choice(consonants) 53 | consecutive_consonants += 1 54 | consecutive_vowels = 0 55 | 56 | name.append(char) 57 | 58 | # Convert to caps or lowercase based on random choice 59 | name = "".join(name) 60 | if use_caps: 61 | name = name.upper() 62 | 63 | # Randomly decide if first letter should be capitalized 64 | if random.choice([True, False]): 65 | name = name.capitalize() 66 | 67 | # Add 1-3 random digits at the end 68 | num_digits = random.randint(1, 3) 69 | name += "".join(random.choices(digits, k=num_digits)) 70 | 71 | return name 72 | 73 | @retry_async(default_value=False) 74 | async def claim_clusters(self) -> bool: 75 | try: 76 | auth_token = await self._login() 77 | if auth_token is None: 78 | raise Exception("Clusters login error") 79 | 80 | cluster_name = None 81 | for _ in range(10): 82 | cluster_name = self._generate_random_name() 83 | is_available = await self._check_if_available(cluster_name) 84 | if is_available: 85 | break 86 | 87 | if cluster_name is None: 88 | logger.error( 89 | f"{self.camp_network.account_index} | Unable to generate cluster name." 90 | ) 91 | return False 92 | 93 | # TODO: Add captcha key 94 | solvium = Solvium( 95 | api_key=self.camp_network.config.CAPTCHA.SOLVIUM_API_KEY, 96 | session=self.camp_network.session, 97 | proxy=self.camp_network.proxy, 98 | ) 99 | 100 | # For enterprise reCAPTCHA v3 101 | captcha_token = await solvium.solve_recaptcha_v3( 102 | sitekey="6Lf7zhkrAAAAAL9QP8CptZhGtgOp-lA5Oi3VGlu5", 103 | pageurl="https://clusters.xyz", 104 | action="SIGNUP", 105 | enterprise=True, 106 | ) 107 | # capsolver = Capsolver( 108 | # api_key="CAP-X", 109 | # proxy=self.camp_network.proxy, 110 | # session=self.camp_network.session, 111 | # ) 112 | # captcha_token = await capsolver.solve_recaptcha( 113 | # sitekey="6Lf7zhkrAAAAAL9QP8CptZhGtgOp-lA5Oi3VGlu5", 114 | # pageurl="https://clusters.xyz", 115 | # page_action="SIGNUP", 116 | # enterprise=True, 117 | # ) 118 | if captcha_token is None: 119 | raise Exception("Unable to solve captcha") 120 | 121 | logger.success( 122 | f"{self.camp_network.account_index} | Captcha for Clusters solved successfully!" 123 | ) 124 | 125 | headers = { 126 | "accept": "*/*", 127 | "accept-language": "ru,en-US;q=0.9,en;q=0.8,ru-RU;q=0.7,zh-TW;q=0.6,zh;q=0.5,uk;q=0.4", 128 | "authorization": f"Bearer {auth_token}", 129 | "content-type": "application/json", 130 | "origin": "https://clusters.xyz", 131 | "priority": "u=1, i", 132 | "referer": "https://clusters.xyz/", 133 | "sec-ch-ua": '"Chromium";v="133", "Google Chrome";v="133", "Not.A/Brand";v="99"', 134 | "sec-ch-ua-mobile": "?0", 135 | "sec-ch-ua-platform": '"Windows"', 136 | "sec-fetch-dest": "empty", 137 | "sec-fetch-mode": "cors", 138 | "sec-fetch-site": "same-site", 139 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", 140 | "x-testnet": "false", 141 | } 142 | 143 | json_data = { 144 | "clusterName": "campnetwork", 145 | "walletName": cluster_name, 146 | "recaptchaToken": { 147 | "type": "invisible", 148 | "token": captcha_token, 149 | }, 150 | } 151 | 152 | response = await self.camp_network.session.post( 153 | "https://api.clusters.xyz/v1/trpc/names.community.register", 154 | headers=headers, 155 | json=json_data, 156 | ) 157 | 158 | if response.status_code != 200: 159 | raise Exception(f"Clusters claim error: {response.text}") 160 | 161 | if ( 162 | response.json()["result"]["data"]["clusterName"] 163 | == f"campnetwork/{cluster_name}" 164 | ): 165 | logger.success( 166 | f"{self.camp_network.account_index} | Clusters claimed successfully!" 167 | ) 168 | return True 169 | 170 | except Exception as e: 171 | random_pause = random.randint( 172 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[0], 173 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[1], 174 | ) 175 | logger.error( 176 | f"{self.camp_network.account_index} | Clusters claim error: {e}. Sleeping {random_pause} seconds..." 177 | ) 178 | await asyncio.sleep(random_pause) 179 | raise 180 | 181 | @retry_async(default_value=False) 182 | async def _check_if_available(self, cluster_name: str) -> bool: 183 | try: 184 | headers = { 185 | "accept": "*/*", 186 | "accept-language": "ru,en-US;q=0.9,en;q=0.8,ru-RU;q=0.7,zh-TW;q=0.6,zh;q=0.5,uk;q=0.4", 187 | "content-type": "application/json", 188 | "origin": "https://clusters.xyz", 189 | "priority": "u=1, i", 190 | "referer": "https://clusters.xyz/", 191 | "sec-ch-ua": '"Chromium";v="133", "Google Chrome";v="133", "Not.A/Brand";v="99"', 192 | "sec-ch-ua-mobile": "?0", 193 | "sec-ch-ua-platform": '"Windows"', 194 | "sec-fetch-dest": "empty", 195 | "sec-fetch-mode": "cors", 196 | "sec-fetch-site": "same-site", 197 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", 198 | "x-testnet": "false", 199 | } 200 | 201 | params = { 202 | "input": '{"clusterName":"campnetwork","name":"' + cluster_name + '"}', 203 | } 204 | 205 | response = await self.camp_network.session.get( 206 | "https://api.clusters.xyz/v1/trpc/names.community.isAvailable", 207 | params=params, 208 | headers=headers, 209 | ) 210 | 211 | if response.status_code != 200: 212 | raise Exception(f"Clusters check error: {response.text}") 213 | 214 | if response.json()["result"]["data"]["isAvailable"]: 215 | return True 216 | else: 217 | return False 218 | 219 | except Exception as e: 220 | random_pause = random.randint( 221 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[0], 222 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[1], 223 | ) 224 | logger.error( 225 | f"{self.camp_network.account_index} | Clusters check error: {e}. Sleeping {random_pause} seconds..." 226 | ) 227 | await asyncio.sleep(random_pause) 228 | raise 229 | 230 | @retry_async(default_value=None) 231 | async def _login(self) -> str: 232 | try: 233 | headers = { 234 | "accept": "*/*", 235 | "accept-language": "ru,en-US;q=0.9,en;q=0.8,ru-RU;q=0.7,zh-TW;q=0.6,zh;q=0.5,uk;q=0.4", 236 | "content-type": "application/json", 237 | "origin": "https://clusters.xyz", 238 | "priority": "u=1, i", 239 | "referer": "https://clusters.xyz/", 240 | "sec-ch-ua": '"Chromium";v="133", "Google Chrome";v="133", "Not.A/Brand";v="99"', 241 | "sec-ch-ua-mobile": "?0", 242 | "sec-ch-ua-platform": '"Windows"', 243 | "sec-fetch-dest": "empty", 244 | "sec-fetch-mode": "cors", 245 | "sec-fetch-site": "same-site", 246 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", 247 | "x-testnet": "false", 248 | } 249 | 250 | signing_date = ( 251 | datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" 252 | ) 253 | 254 | message = ( 255 | f"clusters.xyz verification\n\n" 256 | "Before interacting with certain functionality, we require a wallet signature for verification.\n\n" 257 | f"{signing_date}" 258 | ) 259 | signature = "0x" + self.camp_network.web3.get_signature( 260 | message, self.camp_network.wallet 261 | ) 262 | 263 | params = { 264 | "input": '{"signature":"' 265 | + signature 266 | + '","signingDate":"' 267 | + signing_date 268 | + '","type":"evm","wallet":"' 269 | + self.camp_network.wallet.address 270 | + '"}', 271 | } 272 | 273 | response = await self.camp_network.session.get( 274 | "https://api.clusters.xyz/v1/trpc/auth.getToken", 275 | params=params, 276 | headers=headers, 277 | ) 278 | 279 | if response.status_code != 200: 280 | raise Exception(f"Clusters login error: {response.text}") 281 | 282 | auth_token = response.json()["result"]["data"]["token"] 283 | return auth_token 284 | 285 | except Exception as e: 286 | random_pause = random.randint( 287 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[0], 288 | self.camp_network.config.SETTINGS.PAUSE_BETWEEN_ATTEMPTS[1], 289 | ) 290 | logger.error( 291 | f"{self.camp_network.account_index} | Clusters login error: {e}. Sleeping {random_pause} seconds..." 292 | ) 293 | await asyncio.sleep(random_pause) 294 | raise 295 | -------------------------------------------------------------------------------- /src/model/database/db_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import random 4 | from typing import List 5 | from tabulate import tabulate 6 | from loguru import logger 7 | 8 | from src.model.database.instance import Database 9 | from src.utils.config import get_config 10 | from src.utils.reader import read_private_keys 11 | from src.utils.proxy_parser import Proxy # Добавляем импорт 12 | 13 | 14 | async def show_database_menu(): 15 | while True: 16 | print("\nDatabase Management Options:\n") 17 | print("[1] 🗑 Create/Reset Database") 18 | print("[2] ➕ Generate New Tasks for Completed Wallets") 19 | print("[3] 📊 Show Database Contents") 20 | print("[4] 🔄 Regenerate Tasks for All Wallets") 21 | print("[5] 📝 Add Wallets to Database") 22 | print("[6] 👋 Exit") 23 | print() 24 | 25 | try: 26 | choice = input("Enter option (1-6): ").strip() 27 | 28 | if choice == "1": 29 | await reset_database() 30 | elif choice == "2": 31 | await regenerate_tasks_for_completed() 32 | elif choice == "3": 33 | await show_database_contents() 34 | elif choice == "4": 35 | await regenerate_tasks_for_all() 36 | elif choice == "5": 37 | await add_new_wallets() 38 | elif choice == "6": 39 | print("\nExiting database management...") 40 | break 41 | else: 42 | logger.error("Invalid choice. Please enter a number between 1 and 6.") 43 | 44 | except Exception as e: 45 | logger.error(f"Error in database management: {e}") 46 | await asyncio.sleep(1) 47 | 48 | 49 | async def reset_database(): 50 | """Создание новой или сброс существующей базы данных""" 51 | print("\n⚠️ WARNING: This will delete all existing data.") 52 | print("[1] Yes") 53 | print("[2] No") 54 | 55 | confirmation = input("\nEnter your choice (1-2): ").strip() 56 | 57 | if confirmation != "1": 58 | logger.info("Database reset cancelled") 59 | return 60 | 61 | try: 62 | db = Database() 63 | await db.clear_database() 64 | await db.init_db() 65 | 66 | # Генерируем задачи для новой базы данных 67 | config = get_config() 68 | private_keys = read_private_keys("data/private_keys.txt") 69 | 70 | # Читаем прокси 71 | try: 72 | proxy_objects = Proxy.from_file("data/proxies.txt") 73 | proxies = [proxy.get_default_format() for proxy in proxy_objects] 74 | if len(proxies) == 0: 75 | logger.error("No proxies found in data/proxies.txt") 76 | return 77 | except Exception as e: 78 | logger.error(f"Failed to load proxies: {e}") 79 | return 80 | 81 | # Добавляем кошельки с прокси и задачами 82 | for i, private_key in enumerate(private_keys): 83 | proxy = proxies[i % len(proxies)] 84 | 85 | # Генерируем новый список задач для каждого кошелька 86 | tasks = generate_tasks_from_config(config) 87 | 88 | if not tasks: 89 | logger.error( 90 | f"No tasks generated for wallet {private_key[:4]}...{private_key[-4:]}" 91 | ) 92 | continue 93 | 94 | await db.add_wallet( 95 | private_key=private_key, 96 | proxy=proxy, 97 | tasks_list=tasks, # Передаем сгенерированный список задач 98 | ) 99 | 100 | logger.success( 101 | f"Database has been reset and initialized with {len(private_keys)} wallets!" 102 | ) 103 | 104 | except Exception as e: 105 | logger.error(f"Error resetting database: {e}") 106 | 107 | 108 | def generate_tasks_from_config(config) -> List[str]: 109 | """Генерация списка задач из конфига в том же формате, что и в start.py""" 110 | planned_tasks = [] 111 | 112 | # Получаем список задач из конфига 113 | for task_name in config.FLOW.TASKS: 114 | # Импортируем tasks.py для получения конкретного списка задач 115 | import tasks 116 | 117 | # Получаем список подзадач для текущей задачи 118 | task_list = getattr(tasks, task_name) 119 | 120 | # Обрабатываем каждую подзадачу 121 | for task_item in task_list: 122 | if isinstance(task_item, list): 123 | # Для задач в [], выбираем случайную 124 | selected_task = random.choice(task_item) 125 | planned_tasks.append(selected_task) 126 | elif isinstance(task_item, tuple): 127 | # Для задач в (), перемешиваем все 128 | shuffled_tasks = list(task_item) 129 | random.shuffle(shuffled_tasks) 130 | # Добавляем все задачи из кортежа 131 | planned_tasks.extend(shuffled_tasks) 132 | else: 133 | # Обычная задача 134 | planned_tasks.append(task_item) 135 | 136 | logger.info(f"Generated tasks sequence: {planned_tasks}") 137 | return planned_tasks 138 | 139 | 140 | async def regenerate_tasks_for_completed(): 141 | """Генерация новых задач для завершенных кошельков""" 142 | try: 143 | db = Database() 144 | config = get_config() 145 | 146 | # Получаем список завершенных кошельков 147 | completed_wallets = await db.get_completed_wallets() 148 | 149 | if not completed_wallets: 150 | logger.info("No completed wallets found") 151 | return 152 | 153 | print("\n[1] Yes") 154 | print("[2] No") 155 | confirmation = input( 156 | "\nThis will replace all tasks for completed wallets. Continue? (1-2): " 157 | ).strip() 158 | 159 | if confirmation != "1": 160 | logger.info("Task regeneration cancelled") 161 | return 162 | 163 | # Для каждого завершенного кошелька генерируем новые задачи 164 | for wallet in completed_wallets: 165 | # Генерируем новый список задач 166 | new_tasks = generate_tasks_from_config(config) 167 | 168 | # Очищаем старые задачи и добавляем новые 169 | await db.clear_wallet_tasks(wallet["private_key"]) 170 | await db.add_tasks_to_wallet(wallet["private_key"], new_tasks) 171 | 172 | logger.success( 173 | f"Generated new tasks for {len(completed_wallets)} completed wallets" 174 | ) 175 | 176 | except Exception as e: 177 | logger.error(f"Error regenerating tasks: {e}") 178 | 179 | 180 | async def regenerate_tasks_for_all(): 181 | """Генерация новых задач для всех кошельков""" 182 | try: 183 | db = Database() 184 | config = get_config() 185 | 186 | # Получаем все кошельки 187 | completed_wallets = await db.get_completed_wallets() 188 | uncompleted_wallets = await db.get_uncompleted_wallets() 189 | all_wallets = completed_wallets + uncompleted_wallets 190 | 191 | if not all_wallets: 192 | logger.info("No wallets found in database") 193 | return 194 | 195 | print("\n[1] Yes") 196 | print("[2] No") 197 | confirmation = input( 198 | "\nThis will replace all tasks for ALL wallets. Continue? (1-2): " 199 | ).strip() 200 | 201 | if confirmation != "1": 202 | logger.info("Task regeneration cancelled") 203 | return 204 | 205 | # Для каждого кошелька генерируем новые задачи 206 | for wallet in all_wallets: 207 | # Генерируем новый список задач 208 | new_tasks = generate_tasks_from_config(config) 209 | 210 | # Очищаем старые задачи и добавляем новые 211 | await db.clear_wallet_tasks(wallet["private_key"]) 212 | await db.add_tasks_to_wallet(wallet["private_key"], new_tasks) 213 | 214 | logger.success(f"Generated new tasks for all {len(all_wallets)} wallets") 215 | 216 | except Exception as e: 217 | logger.error(f"Error regenerating tasks for all wallets: {e}") 218 | 219 | 220 | async def show_database_contents(): 221 | """Отображение содержимого базы данных в табличном формате""" 222 | try: 223 | db = Database() 224 | 225 | # Получаем все кошельки 226 | completed_wallets = await db.get_completed_wallets() 227 | uncompleted_wallets = await db.get_uncompleted_wallets() 228 | all_wallets = completed_wallets + uncompleted_wallets 229 | 230 | if not all_wallets: 231 | logger.info("Database is empty") 232 | return 233 | 234 | # Подготавливаем данные для таблицы 235 | table_data = [] 236 | for wallet in all_wallets: 237 | tasks = ( 238 | json.loads(wallet["tasks"]) 239 | if isinstance(wallet["tasks"], str) 240 | else wallet["tasks"] 241 | ) 242 | 243 | # Форматируем список задач 244 | completed_tasks = [ 245 | task["name"] for task in tasks if task["status"] == "completed" 246 | ] 247 | pending_tasks = [ 248 | task["name"] for task in tasks if task["status"] == "pending" 249 | ] 250 | 251 | # Сокращаем private key для отображения 252 | short_key = f"{wallet['private_key'][:6]}...{wallet['private_key'][-4:]}" 253 | 254 | # Форматируем прокси для отображения 255 | proxy = wallet["proxy"] 256 | if proxy and len(proxy) > 20: 257 | proxy = f"{proxy[:17]}..." 258 | 259 | table_data.append( 260 | [ 261 | short_key, 262 | proxy or "No proxy", 263 | wallet["status"], 264 | f"{len(completed_tasks)}/{len(tasks)}", 265 | ", ".join(completed_tasks) or "None", 266 | ", ".join(pending_tasks) or "None", 267 | ] 268 | ) 269 | 270 | # Создаем таблицу 271 | headers = [ 272 | "Wallet", 273 | "Proxy", 274 | "Status", 275 | "Progress", 276 | "Completed Tasks", 277 | "Pending Tasks", 278 | ] 279 | table = tabulate(table_data, headers=headers, tablefmt="grid", stralign="left") 280 | 281 | # Выводим статистику 282 | total_wallets = len(all_wallets) 283 | completed_count = len(completed_wallets) 284 | print(f"\nDatabase Statistics:") 285 | print(f"Total Wallets: {total_wallets}") 286 | print(f"Completed Wallets: {completed_count}") 287 | print(f"Pending Wallets: {total_wallets - completed_count}") 288 | 289 | # Выводим таблицу 290 | print("\nDatabase Contents:") 291 | print(table) 292 | 293 | except Exception as e: 294 | logger.error(f"Error showing database contents: {e}") 295 | 296 | 297 | async def add_new_wallets(): 298 | """Добавление новых кошельков из файла в базу данных""" 299 | try: 300 | db = Database() 301 | config = get_config() 302 | 303 | # Читаем все приватные ключи из файла 304 | private_keys = read_private_keys("data/private_keys.txt") 305 | 306 | # Читаем прокси 307 | try: 308 | proxy_objects = Proxy.from_file("data/proxies.txt") 309 | proxies = [proxy.get_default_format() for proxy in proxy_objects] 310 | if len(proxies) == 0: 311 | logger.error("No proxies found in data/proxies.txt") 312 | return 313 | except Exception as e: 314 | logger.error(f"Failed to load proxies: {e}") 315 | return 316 | 317 | # Получаем существующие кошельки из базы 318 | completed_wallets = await db.get_completed_wallets() 319 | uncompleted_wallets = await db.get_uncompleted_wallets() 320 | existing_wallets = { 321 | w["private_key"] for w in (completed_wallets + uncompleted_wallets) 322 | } 323 | 324 | # Находим новые кошельки 325 | new_wallets = [pk for pk in private_keys if pk not in existing_wallets] 326 | 327 | if not new_wallets: 328 | logger.info("No new wallets found to add") 329 | return 330 | 331 | print(f"\nFound {len(new_wallets)} new wallets to add to database") 332 | print("\n[1] Yes") 333 | print("[2] No") 334 | confirmation = input("\nDo you want to add these wallets? (1-2): ").strip() 335 | 336 | if confirmation != "1": 337 | logger.info("Adding new wallets cancelled") 338 | return 339 | 340 | # Добавляем новые кошельки 341 | added_count = 0 342 | for private_key in new_wallets: 343 | proxy = proxies[added_count % len(proxies)] 344 | tasks = generate_tasks_from_config(config) 345 | 346 | if not tasks: 347 | logger.error( 348 | f"No tasks generated for wallet {private_key[:4]}...{private_key[-4:]}" 349 | ) 350 | continue 351 | 352 | await db.add_wallet( 353 | private_key=private_key, 354 | proxy=proxy, 355 | tasks_list=tasks, 356 | ) 357 | added_count += 1 358 | 359 | logger.success(f"Successfully added {added_count} new wallets to database!") 360 | 361 | except Exception as e: 362 | logger.error(f"Error adding new wallets: {e}") 363 | -------------------------------------------------------------------------------- /src/model/start.py: -------------------------------------------------------------------------------- 1 | from eth_account import Account 2 | from loguru import logger 3 | import primp 4 | import random 5 | import asyncio 6 | 7 | from src.model.offchain.cex.instance import CexWithdraw 8 | from src.model.projects.crustyswap.instance import CrustySwap 9 | from src.model.projects.camp_loyalty.instance import CampLoyalty 10 | from src.model.camp_network import CampNetwork 11 | from src.model.help.stats import WalletStats 12 | from src.model.onchain.web3_custom import Web3Custom 13 | from src.utils.client import create_client 14 | from src.utils.config import Config 15 | from src.model.database.db_manager import Database 16 | from src.utils.telegram_logger import send_telegram_message 17 | from src.utils.decorators import retry_async 18 | from src.utils.reader import read_private_keys 19 | 20 | 21 | class Start: 22 | def __init__( 23 | self, 24 | account_index: int, 25 | proxy: str, 26 | private_key: str, 27 | config: Config, 28 | discord_token: str, 29 | twitter_token: str, 30 | email: str, 31 | ): 32 | self.account_index = account_index 33 | self.proxy = proxy 34 | self.private_key = private_key 35 | self.config = config 36 | self.discord_token = discord_token 37 | self.twitter_token = twitter_token 38 | self.email = email 39 | 40 | self.session: primp.AsyncClient | None = None 41 | self.camp_web3: Web3Custom | None = None 42 | self.camp_instance: CampNetwork | None = None 43 | self.loyalty: CampLoyalty | None = None 44 | 45 | self.wallet = Account.from_key(self.private_key) 46 | self.wallet_address = self.wallet.address 47 | 48 | @retry_async(default_value=False) 49 | async def initialize(self): 50 | try: 51 | self.session = await create_client( 52 | self.proxy, self.config.OTHERS.SKIP_SSL_VERIFICATION 53 | ) 54 | self.camp_web3 = await Web3Custom.create( 55 | self.account_index, 56 | self.config.RPCS.CAMP_NETWORK, 57 | self.config.OTHERS.USE_PROXY_FOR_RPC, 58 | self.proxy, 59 | self.config.OTHERS.SKIP_SSL_VERIFICATION, 60 | ) 61 | 62 | self.camp_instance = CampNetwork( 63 | self.account_index, 64 | self.session, 65 | self.camp_web3, 66 | self.config, 67 | self.wallet, 68 | self.discord_token, 69 | self.twitter_token, 70 | self.proxy, 71 | self.private_key, 72 | self.email, 73 | ) 74 | 75 | return True 76 | except Exception as e: 77 | logger.error(f"{self.account_index} | Error: {e}") 78 | raise 79 | 80 | async def flow(self): 81 | try: 82 | db = Database() 83 | try: 84 | tasks = await db.get_wallet_pending_tasks(self.private_key) 85 | except Exception as e: 86 | if "no such table: wallets" in str(e): 87 | logger.error( 88 | f"🔴 [{self.account_index}] Database error: Database not created or wallets table not found. Please check the tutorial on how to create a database." 89 | ) 90 | if self.config.SETTINGS.SEND_TELEGRAM_LOGS: 91 | error_message = ( 92 | f"⚠️ Database Error Alert\n\n" 93 | f"👤 Account #{self.account_index}\n" 94 | f"💳 Wallet: {self.private_key[:6]}...{self.private_key[-4:]}\n" 95 | f"❌ Error: Database not created or wallets table not found" 96 | ) 97 | await send_telegram_message(self.config, error_message) 98 | return False 99 | else: 100 | logger.error( 101 | f"🔴 [{self.account_index}] Failed to fetch tasks from database: {e}" 102 | ) 103 | raise 104 | 105 | if not tasks: 106 | logger.warning( 107 | f"⚠️ [{self.account_index}] No pending tasks found in database for this wallet. Exiting..." 108 | ) 109 | if self.camp_web3: 110 | await self.camp_web3.cleanup() 111 | return True 112 | 113 | pause = random.randint( 114 | self.config.SETTINGS.RANDOM_INITIALIZATION_PAUSE[0], 115 | self.config.SETTINGS.RANDOM_INITIALIZATION_PAUSE[1], 116 | ) 117 | logger.info( 118 | f"⏳ [{self.account_index}] Initial pause: {pause} seconds before starting..." 119 | ) 120 | await asyncio.sleep(pause) 121 | 122 | task_plan_msg = [f"{i+1}. {task['name']}" for i, task in enumerate(tasks)] 123 | logger.info( 124 | f"📋 [{self.account_index}] Task execution plan: {' | '.join(task_plan_msg)}" 125 | ) 126 | 127 | completed_tasks = [] 128 | failed_tasks = [] 129 | 130 | # login to camp loyalty 131 | for task in tasks: 132 | if task["name"].lower().startswith("camp_loyalty"): 133 | self.loyalty = CampLoyalty(self.camp_instance) 134 | if not await self.loyalty.login(): 135 | logger.error( 136 | f"🔴 [{self.account_index}] Failed to login to CampLoyalty" 137 | ) 138 | self.loyalty = None 139 | break 140 | 141 | # Execute tasks 142 | for task in tasks: 143 | task_name = task["name"] 144 | if task_name == "skip": 145 | logger.info(f"⏭️ [{self.account_index}] Skipping task: {task_name}") 146 | await db.update_task_status( 147 | self.private_key, task_name, "completed" 148 | ) 149 | completed_tasks.append(task_name) 150 | await self.sleep(task_name) 151 | continue 152 | 153 | logger.info(f"🚀 [{self.account_index}] Executing task: {task_name}") 154 | 155 | success = await self.execute_task(task_name) 156 | 157 | if success: 158 | await db.update_task_status( 159 | self.private_key, task_name, "completed" 160 | ) 161 | completed_tasks.append(task_name) 162 | await self.sleep(task_name) 163 | else: 164 | failed_tasks.append(task_name) 165 | if not self.config.FLOW.SKIP_FAILED_TASKS: 166 | logger.error( 167 | f"🔴 [{self.account_index}] Task {task_name} failed. Stopping wallet execution." 168 | ) 169 | break 170 | else: 171 | logger.warning( 172 | f"⚠️ [{self.account_index}] Task {task_name} failed. Continuing to next task." 173 | ) 174 | await self.sleep(task_name) 175 | 176 | try: 177 | wallet_stats = WalletStats(self.config, self.camp_web3) 178 | await wallet_stats.get_wallet_stats( 179 | self.private_key, self.account_index 180 | ) 181 | except Exception as e: 182 | pass 183 | 184 | # Send Telegram message at the end 185 | if self.config.SETTINGS.SEND_TELEGRAM_LOGS: 186 | message = ( 187 | f"🤖 StarLabs CampNetwork Bot Report\n\n" 188 | f"👤 Account: #{self.account_index}\n" 189 | f"💳 Wallet: {self.private_key[:6]}...{self.private_key[-4:]}\n\n" 190 | ) 191 | 192 | if completed_tasks: 193 | message += f"✅ Completed Tasks:\n" 194 | for i, task in enumerate(completed_tasks, 1): 195 | message += f"{i}. {task}\n" 196 | message += "\n" 197 | 198 | if failed_tasks: 199 | message += f"❌ Failed Tasks:\n" 200 | for i, task in enumerate(failed_tasks, 1): 201 | message += f"{i}. {task}\n" 202 | message += "\n" 203 | 204 | total_tasks = len(tasks) 205 | completed_count = len(completed_tasks) 206 | message += ( 207 | f"📊 Statistics:\n" 208 | f"📝 Total Tasks: {total_tasks}\n" 209 | f"✅ Completed: {completed_count}\n" 210 | f"❌ Failed: {len(failed_tasks)}\n" 211 | f"📈 Success Rate: {(completed_count/total_tasks)*100:.1f}%\n\n" 212 | f"⚙️ Settings:\n" 213 | f"⏭️ Skip Failed: {'Yes' if self.config.FLOW.SKIP_FAILED_TASKS else 'No'}\n" 214 | ) 215 | 216 | await send_telegram_message(self.config, message) 217 | 218 | return len(failed_tasks) == 0 219 | 220 | except Exception as e: 221 | logger.error(f"🔴 [{self.account_index}] Critical error: {e}") 222 | 223 | if self.config.SETTINGS.SEND_TELEGRAM_LOGS: 224 | error_message = ( 225 | f"⚠️ Critical Error Alert\n\n" 226 | f"👤 Account #{self.account_index}\n" 227 | f"💳 Wallet: {self.private_key[:6]}...{self.private_key[-4:]}\n" 228 | f"❌ Error: {str(e)}" 229 | ) 230 | await send_telegram_message(self.config, error_message) 231 | 232 | return False 233 | finally: 234 | # Cleanup resources 235 | try: 236 | if self.camp_web3: 237 | await self.camp_web3.cleanup() 238 | logger.info( 239 | f"✨ [{self.account_index}] All sessions closed successfully" 240 | ) 241 | except Exception as e: 242 | logger.error(f"🔴 [{self.account_index}] Cleanup error: {e}") 243 | 244 | pause = random.randint( 245 | self.config.SETTINGS.RANDOM_PAUSE_BETWEEN_ACCOUNTS[0], 246 | self.config.SETTINGS.RANDOM_PAUSE_BETWEEN_ACCOUNTS[1], 247 | ) 248 | logger.info( 249 | f"⏳ [{self.account_index}] Final pause: {pause} seconds before next account..." 250 | ) 251 | await asyncio.sleep(pause) 252 | 253 | async def execute_task(self, task): 254 | """Execute a single task""" 255 | task = task.lower() 256 | 257 | if task == "faucet": 258 | return await self.camp_instance.request_faucet() 259 | 260 | if task == "crusty_refuel": 261 | crusty_swap = CrustySwap( 262 | self.account_index, 263 | self.session, 264 | self.camp_web3, 265 | self.config, 266 | self.wallet, 267 | self.proxy, 268 | self.private_key, 269 | ) 270 | return await crusty_swap.refuel() 271 | 272 | if task == "crusty_refuel_from_one_to_all": 273 | private_keys = read_private_keys("data/private_keys.txt") 274 | 275 | crusty_swap = CrustySwap( 276 | 1, 277 | self.session, 278 | self.camp_web3, 279 | self.config, 280 | Account.from_key(private_keys[0]), 281 | self.proxy, 282 | private_keys[0], 283 | ) 284 | private_keys = private_keys[1:] 285 | return await crusty_swap.refuel_from_one_to_all(private_keys) 286 | 287 | if task == "cex_withdrawal": 288 | cex_withdrawal = CexWithdraw( 289 | self.account_index, 290 | self.private_key, 291 | self.config, 292 | ) 293 | return await cex_withdrawal.withdraw() 294 | 295 | if task.startswith("camp_loyalty"): 296 | if not self.loyalty: 297 | logger.error( 298 | f"🔴 [{self.account_index}] CampLoyalty login failed. Skipping task..." 299 | ) 300 | return False 301 | 302 | if task == "camp_loyalty_connect_socials": 303 | return await self.loyalty.connect_socials() 304 | 305 | if task == "camp_loyalty_set_display_name": 306 | return await self.loyalty.set_display_name() 307 | 308 | # if task == "camp_loyalty_set_email": 309 | # return await self.loyalty.set_email() 310 | 311 | if task.startswith("camp_loyalty_"): 312 | return await self.loyalty.complete_quests(task) 313 | 314 | logger.error(f"🔴 [{self.account_index}] Unknown task type: {task}") 315 | return False 316 | 317 | async def sleep(self, task_name: str): 318 | """Makes a random pause between actions""" 319 | pause = random.randint( 320 | self.config.SETTINGS.RANDOM_PAUSE_BETWEEN_ACTIONS[0], 321 | self.config.SETTINGS.RANDOM_PAUSE_BETWEEN_ACTIONS[1], 322 | ) 323 | logger.info( 324 | f"⏳ [{self.account_index}] Pausing {pause} seconds after {task_name}" 325 | ) 326 | await asyncio.sleep(pause) 327 | -------------------------------------------------------------------------------- /src/model/database/instance.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Optional, List, Dict 3 | from sqlalchemy import create_engine, Column, Integer, String 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession 6 | from sqlalchemy.orm import sessionmaker 7 | from loguru import logger 8 | 9 | Base = declarative_base() 10 | 11 | 12 | class Wallet(Base): 13 | __tablename__ = "wallets" 14 | id = Column(Integer, primary_key=True) 15 | private_key = Column(String, unique=True) 16 | proxy = Column(String, nullable=True) 17 | status = Column(String) # общий статус кошелька (pending/completed) 18 | tasks = Column(String) # JSON строка с задачами 19 | 20 | 21 | class Database: 22 | def __init__(self): 23 | self.engine = create_async_engine( 24 | "sqlite+aiosqlite:///data/accounts.db", # Изменен путь и название БД 25 | echo=False, 26 | ) 27 | self.session = sessionmaker( 28 | bind=self.engine, class_=AsyncSession, expire_on_commit=False 29 | ) 30 | 31 | async def init_db(self): 32 | """Инициализация базы данных""" 33 | async with self.engine.begin() as conn: 34 | await conn.run_sync(Base.metadata.create_all) 35 | logger.success("Database initialized successfully") 36 | 37 | async def clear_database(self): 38 | """Полная очистка базы данных""" 39 | async with self.engine.begin() as conn: 40 | await conn.run_sync(Base.metadata.drop_all) 41 | await conn.run_sync(Base.metadata.create_all) 42 | logger.success("Database cleared successfully") 43 | 44 | async def add_wallet( 45 | self, 46 | private_key: str, 47 | proxy: Optional[str] = None, 48 | tasks_list: Optional[List[str]] = None, 49 | ) -> None: 50 | """ 51 | Добавление нового кошелька 52 | 53 | :param private_key: Приватный ключ кошелька 54 | :param proxy: Прокси (опционально) 55 | :param tasks_list: Список названий задач 56 | """ 57 | # Преобразуем список задач в нужный формат для БД 58 | tasks = [] 59 | for task in tasks_list or []: 60 | tasks.append( 61 | { 62 | "name": task, 63 | "status": "pending", 64 | "index": len(tasks) + 1, # Добавляем индекс для сохранения порядка 65 | } 66 | ) 67 | 68 | async with self.session() as session: 69 | wallet = Wallet( 70 | private_key=private_key, 71 | proxy=proxy, 72 | status="pending", 73 | tasks=json.dumps(tasks), 74 | ) 75 | session.add(wallet) 76 | await session.commit() 77 | logger.success(f"Added wallet {private_key[:4]}...{private_key[-4:]}") 78 | 79 | async def update_task_status( 80 | self, private_key: str, task_name: str, new_status: str 81 | ) -> None: 82 | """ 83 | Обновление статуса конкретной задачи 84 | 85 | :param private_key: Приватный ключ кошелька 86 | :param task_name: Название задачи 87 | :param new_status: Новый статус (pending/completed) 88 | """ 89 | async with self.session() as session: 90 | wallet = await self._get_wallet(session, private_key) 91 | if not wallet: 92 | logger.error(f"Wallet {private_key[:4]}...{private_key[-4:]} not found") 93 | return 94 | 95 | tasks = json.loads(wallet.tasks) 96 | for task in tasks: 97 | if task["name"] == task_name: 98 | task["status"] = new_status 99 | break 100 | 101 | wallet.tasks = json.dumps(tasks) 102 | 103 | # Проверяем, все ли задачи выполнены 104 | if all(task["status"] == "completed" for task in tasks): 105 | wallet.status = "completed" 106 | 107 | await session.commit() 108 | logger.info( 109 | f"Updated task {task_name} to {new_status} for wallet {private_key[:4]}...{private_key[-4:]}" 110 | ) 111 | 112 | async def clear_wallet_tasks(self, private_key: str) -> None: 113 | """ 114 | Очистка всех задач кошелька 115 | 116 | :param private_key: Приватный ключ кошелька 117 | """ 118 | async with self.session() as session: 119 | wallet = await self._get_wallet(session, private_key) 120 | if not wallet: 121 | return 122 | 123 | wallet.tasks = json.dumps([]) 124 | wallet.status = "pending" 125 | await session.commit() 126 | logger.info( 127 | f"Cleared all tasks for wallet {private_key[:4]}...{private_key[-4:]}" 128 | ) 129 | 130 | async def update_wallet_proxy(self, private_key: str, new_proxy: str) -> None: 131 | """ 132 | Обновление прокси кошелька 133 | 134 | :param private_key: Приватный ключ кошелька 135 | :param new_proxy: Новый прокси 136 | """ 137 | async with self.session() as session: 138 | wallet = await self._get_wallet(session, private_key) 139 | if not wallet: 140 | return 141 | 142 | wallet.proxy = new_proxy 143 | await session.commit() 144 | logger.info( 145 | f"Updated proxy for wallet {private_key[:4]}...{private_key[-4:]}" 146 | ) 147 | 148 | async def get_wallet_tasks(self, private_key: str) -> List[Dict]: 149 | """ 150 | Получение всех задач кошелька 151 | 152 | :param private_key: Приватный ключ кошелька 153 | :return: Список задач с их статусами 154 | """ 155 | async with self.session() as session: 156 | wallet = await self._get_wallet(session, private_key) 157 | if not wallet: 158 | return [] 159 | return json.loads(wallet.tasks) 160 | 161 | async def get_pending_tasks(self, private_key: str) -> List[str]: 162 | """ 163 | Получение всех незавершенных задач кошелька 164 | 165 | :param private_key: Приватный ключ кошелька 166 | :return: Список названий незавершенных задач 167 | """ 168 | tasks = await self.get_wallet_tasks(private_key) 169 | return [task["name"] for task in tasks if task["status"] == "pending"] 170 | 171 | async def get_completed_tasks(self, private_key: str) -> List[str]: 172 | """ 173 | Получение всех завершенных задач кошелька 174 | 175 | :param private_key: Приватный ключ кошелька 176 | :return: Список названий завершенных задач 177 | """ 178 | tasks = await self.get_wallet_tasks(private_key) 179 | return [task["name"] for task in tasks if task["status"] == "completed"] 180 | 181 | async def get_uncompleted_wallets(self) -> List[Dict]: 182 | """ 183 | Получение списка всех кошельков с невыполненными задачами 184 | 185 | :return: Список кошельков с их данными 186 | """ 187 | async with self.session() as session: 188 | from sqlalchemy import select 189 | 190 | query = select(Wallet).filter_by(status="pending") 191 | result = await session.execute(query) 192 | wallets = result.scalars().all() 193 | 194 | # Преобразуем в список словарей для удобства использования 195 | return [ 196 | { 197 | "private_key": wallet.private_key, 198 | "proxy": wallet.proxy, 199 | "status": wallet.status, 200 | "tasks": json.loads(wallet.tasks), 201 | } 202 | for wallet in wallets 203 | ] 204 | 205 | async def get_wallet_status(self, private_key: str) -> Optional[str]: 206 | """ 207 | Получение статуса кошелька 208 | 209 | :param private_key: Приватный ключ кошелька 210 | :return: Статус кошелька или None если кошелёк не найден 211 | """ 212 | async with self.session() as session: 213 | wallet = await self._get_wallet(session, private_key) 214 | return wallet.status if wallet else None 215 | 216 | async def _get_wallet( 217 | self, session: AsyncSession, private_key: str 218 | ) -> Optional[Wallet]: 219 | """Внутренний метод для получения кошелька по private_key""" 220 | from sqlalchemy import select 221 | 222 | result = await session.execute( 223 | select(Wallet).filter_by(private_key=private_key) 224 | ) 225 | return result.scalar_one_or_none() 226 | 227 | async def add_tasks_to_wallet(self, private_key: str, new_tasks: List[str]) -> None: 228 | """ 229 | Добавление новых задач к существующему кошельку 230 | 231 | :param private_key: Приватный ключ кошелька 232 | :param new_tasks: Список новых задач для добавления 233 | """ 234 | async with self.session() as session: 235 | wallet = await self._get_wallet(session, private_key) 236 | if not wallet: 237 | return 238 | 239 | current_tasks = json.loads(wallet.tasks) 240 | current_task_names = {task["name"] for task in current_tasks} 241 | 242 | # Добавляем только новые задачи 243 | for task in new_tasks: 244 | if task not in current_task_names: 245 | current_tasks.append({"name": task, "status": "pending"}) 246 | 247 | wallet.tasks = json.dumps(current_tasks) 248 | wallet.status = ( 249 | "pending" # Если добавили новые задачи, статус снова pending 250 | ) 251 | await session.commit() 252 | logger.info( 253 | f"Added new tasks for wallet {private_key[:4]}...{private_key[-4:]}" 254 | ) 255 | 256 | async def get_completed_wallets_count(self) -> int: 257 | """ 258 | Получение количества кошельков, у которых выполнены все задачи 259 | 260 | :return: Количество завершенных кошельков 261 | """ 262 | async with self.session() as session: 263 | from sqlalchemy import select, func 264 | 265 | query = ( 266 | select(func.count()).select_from(Wallet).filter_by(status="completed") 267 | ) 268 | result = await session.execute(query) 269 | return result.scalar() 270 | 271 | async def get_total_wallets_count(self) -> int: 272 | """ 273 | Получение общего количества кошельков в базе 274 | 275 | :return: Общее количество кошельков 276 | """ 277 | async with self.session() as session: 278 | from sqlalchemy import select, func 279 | 280 | query = select(func.count()).select_from(Wallet) 281 | result = await session.execute(query) 282 | return result.scalar() 283 | 284 | async def get_wallet_completed_tasks(self, private_key: str) -> List[str]: 285 | """ 286 | Получение списка выполненных задач кошелька 287 | 288 | :param private_key: Приватный ключ кошелька 289 | :return: Список названий выполненных задач 290 | """ 291 | tasks = await self.get_wallet_tasks(private_key) 292 | return [task["name"] for task in tasks if task["status"] == "completed"] 293 | 294 | async def get_wallet_pending_tasks(self, private_key: str) -> List[Dict]: 295 | """ 296 | Получение списка невыполненных задач кошелька 297 | 298 | :param private_key: Приватный ключ кошелька 299 | :return: Список задач с их индексами и статусами 300 | """ 301 | tasks = await self.get_wallet_tasks(private_key) 302 | return [task for task in tasks if task["status"] == "pending"] 303 | 304 | async def get_completed_wallets(self) -> List[Dict]: 305 | """ 306 | Получение списка всех кошельков с выполненными задачами 307 | 308 | :return: Список кошельков с их данными 309 | """ 310 | async with self.session() as session: 311 | from sqlalchemy import select 312 | 313 | query = select(Wallet).filter_by(status="completed") 314 | result = await session.execute(query) 315 | wallets = result.scalars().all() 316 | 317 | return [ 318 | { 319 | "private_key": wallet.private_key, 320 | "proxy": wallet.proxy, 321 | "status": wallet.status, 322 | "tasks": json.loads(wallet.tasks), 323 | } 324 | for wallet in wallets 325 | ] 326 | 327 | async def get_wallet_tasks_info(self, private_key: str) -> Dict: 328 | """ 329 | Получение полной информации о задачах кошелька 330 | 331 | :param private_key: Приватный ключ кошелька 332 | :return: Словарь с информацией о задачах 333 | """ 334 | tasks = await self.get_wallet_tasks(private_key) 335 | completed = [task["name"] for task in tasks if task["status"] == "completed"] 336 | pending = [task["name"] for task in tasks if task["status"] == "pending"] 337 | 338 | return { 339 | "total_tasks": len(tasks), 340 | "completed_tasks": completed, 341 | "pending_tasks": pending, 342 | "completed_count": len(completed), 343 | "pending_count": len(pending), 344 | } 345 | 346 | 347 | # # Создание и инициализация БД 348 | # db = Database() 349 | # await db.init_db() 350 | 351 | # # Добавление кошелька с задачами 352 | # await db.add_wallet( 353 | # private_key="0x123...", 354 | # proxy="http://proxy1.com", 355 | # tasks_list=["FAUCET", "OKX_WITHDRAW", "TESTNET_BRIDGE"] 356 | # ) 357 | 358 | # # Обновление статуса задачи 359 | # await db.update_task_status( 360 | # private_key="0x123...", 361 | # task_name="FAUCET", 362 | # new_status="completed" 363 | # ) 364 | 365 | # # Получение списка незавершенных задач 366 | # pending_tasks = await db.get_pending_tasks("0x123...") 367 | 368 | # # Очистка задач кошелька 369 | # await db.clear_wallet_tasks("0x123...") 370 | 371 | # # Добавление новых задач к существующему кошельку 372 | # await db.add_tasks_to_wallet( 373 | # private_key="0x123...", 374 | # new_tasks=["NEW_TASK1", "NEW_TASK2"] 375 | # ) 376 | -------------------------------------------------------------------------------- /src/model/offchain/cex/instance.py: -------------------------------------------------------------------------------- 1 | import random 2 | import ccxt.async_support as ccxt 3 | import asyncio 4 | import time 5 | from decimal import Decimal 6 | from src.utils.config import Config 7 | from eth_account import Account 8 | from loguru import logger 9 | from web3 import Web3 10 | from src.model.offchain.cex.constants import ( 11 | CEX_WITHDRAWAL_RPCS, 12 | NETWORK_MAPPINGS, 13 | EXCHANGE_PARAMS, 14 | SUPPORTED_EXCHANGES 15 | ) 16 | from typing import Dict, Optional 17 | 18 | 19 | class CexWithdraw: 20 | def __init__(self, account_index: int, private_key: str, config: Config): 21 | self.account_index = account_index 22 | self.private_key = private_key 23 | self.config = config 24 | 25 | # Setup exchange based on config 26 | exchange_name = config.EXCHANGES.name.lower() 27 | if exchange_name not in SUPPORTED_EXCHANGES: 28 | raise ValueError(f"Unsupported exchange: {exchange_name}") 29 | 30 | # Initialize exchange 31 | self.exchange = getattr(ccxt, exchange_name)() 32 | 33 | # Setup exchange credentials 34 | self.exchange.apiKey = config.EXCHANGES.apiKey 35 | self.exchange.secret = config.EXCHANGES.secretKey 36 | if config.EXCHANGES.passphrase: 37 | self.exchange.password = config.EXCHANGES.passphrase 38 | 39 | self.account = Account.from_key(private_key) 40 | self.address = self.account.address 41 | 42 | # Get withdrawal network from config 43 | if not self.config.EXCHANGES.withdrawals: 44 | raise ValueError("No withdrawal configurations found") 45 | 46 | withdrawal_config = self.config.EXCHANGES.withdrawals[0] 47 | if not withdrawal_config.networks: 48 | raise ValueError("No networks specified in withdrawal configuration") 49 | 50 | # The network will be selected during withdrawal, not in __init__ 51 | # We'll initialize web3 only after network selection in the withdraw method 52 | self.network = None 53 | self.web3 = None 54 | 55 | async def __aenter__(self): 56 | """Async context manager entry""" 57 | await self.check_auth() 58 | return self 59 | 60 | async def __aexit__(self, exc_type, exc_val, exc_tb): 61 | """Async context manager exit""" 62 | await self.exchange.close() 63 | 64 | async def check_auth(self) -> None: 65 | """Test exchange authentication""" 66 | logger.info(f"[{self.account_index}] Testing exchange authentication...") 67 | try: 68 | await self.exchange.fetch_balance() 69 | logger.success(f"[{self.account_index}] Authentication successful") 70 | except ccxt.AuthenticationError as e: 71 | logger.error(f"[{self.account_index}] Authentication error: {str(e)}") 72 | await self.exchange.close() 73 | raise 74 | except Exception as e: 75 | logger.error(f"[{self.account_index}] Unexpected error during authentication: {str(e)}") 76 | await self.exchange.close() 77 | raise 78 | 79 | async def get_chains_info(self) -> Dict: 80 | """Get withdrawal networks information""" 81 | logger.info(f"[{self.account_index}] Getting withdrawal networks data...") 82 | 83 | try: 84 | await self.exchange.load_markets() 85 | 86 | chains_info = {} 87 | withdrawal_config = self.config.EXCHANGES.withdrawals[0] 88 | currency = withdrawal_config.currency.upper() 89 | 90 | if currency not in self.exchange.currencies: 91 | logger.error(f"[{self.account_index}] Currency {currency} not found on {self.config.EXCHANGES.name}") 92 | return {} 93 | 94 | networks = self.exchange.currencies[currency]["networks"] 95 | # logger.info(f"[{self.account_index}] Available networks for {currency}:") 96 | 97 | for key, info in networks.items(): 98 | withdraw_fee = info["fee"] 99 | withdraw_min = info["limits"]["withdraw"]["min"] 100 | network_id = info["id"] 101 | 102 | logger.info(f"[{self.account_index}] - Network: {key} (ID: {network_id})") 103 | logger.info(f"[{self.account_index}] Fee: {withdraw_fee}, Min Amount: {withdraw_min}") 104 | logger.info(f"[{self.account_index}] Enabled: {info['withdraw']}") 105 | 106 | if info["withdraw"]: 107 | chains_info[key] = { 108 | "chainId": network_id, 109 | "withdrawEnabled": True, 110 | "withdrawFee": withdraw_fee, 111 | "withdrawMin": withdraw_min 112 | } 113 | 114 | return chains_info 115 | except Exception as e: 116 | logger.error(f"[{self.account_index}] Error getting chains info: {str(e)}") 117 | await self.exchange.close() 118 | raise 119 | 120 | def _is_withdrawal_enabled(self, key: str, info: Dict) -> bool: 121 | """Check if withdrawal is enabled for the network""" 122 | return info["withdraw"] 123 | 124 | def _get_chain_id(self, key: str, info: Dict) -> str: 125 | """Get network chain ID""" 126 | return info["id"] 127 | 128 | def _get_withdraw_fee(self, info: Dict) -> float: 129 | """Get withdrawal fee""" 130 | return info["fee"] 131 | 132 | @staticmethod 133 | def _get_withdraw_min(info: Dict) -> float: 134 | """Get minimum withdrawal amount""" 135 | return info["limits"]["withdraw"]["min"] 136 | 137 | async def check_balance(self, amount: float) -> bool: 138 | """Check if exchange has enough balance for withdrawal""" 139 | try: 140 | # Get exchange-specific balance parameters 141 | exchange_name = self.config.EXCHANGES.name.lower() 142 | params = EXCHANGE_PARAMS[exchange_name]["balance"] 143 | 144 | balances = await self.exchange.fetch_balance(params=params) 145 | withdrawal_config = self.config.EXCHANGES.withdrawals[0] 146 | currency = withdrawal_config.currency.upper() 147 | 148 | balance = float(balances[currency]["total"]) 149 | logger.info(f"[{self.account_index}] Exchange balance: {balance:.8f} {currency}") 150 | 151 | if balance < amount: 152 | logger.error(f"[{self.account_index}] Insufficient balance for withdrawal {balance} {currency} < {amount} {currency}") 153 | await self.exchange.close() 154 | return False 155 | 156 | return True 157 | 158 | except Exception as e: 159 | logger.error(f"[{self.account_index}] Error checking balance: {str(e)}") 160 | await self.exchange.close() 161 | raise 162 | 163 | async def get_eth_balance(self) -> Decimal: 164 | """Get ETH balance for the wallet address""" 165 | if self.web3 is None: 166 | raise ValueError(f"[{self.account_index}] Web3 instance not initialized. Network must be selected first.") 167 | 168 | balance_wei = self.web3.eth.get_balance(self.address) 169 | return Decimal(self.web3.from_wei(balance_wei, 'ether')) 170 | 171 | async def wait_for_balance_update(self, initial_balance: Decimal, timeout: int = 600) -> bool: 172 | """ 173 | Wait for the balance to increase from the initial balance. 174 | Returns True if balance increased, False if timeout reached. 175 | """ 176 | start_time = time.time() 177 | logger.info(f"[{self.account_index}] Waiting for funds to arrive. Initial balance: {initial_balance} ETH") 178 | 179 | while time.time() - start_time < timeout: 180 | try: 181 | current_balance = await self.get_eth_balance() 182 | if current_balance > initial_balance: 183 | increase = current_balance - initial_balance 184 | logger.success(f"[{self.account_index}] Funds received! Balance increased by {increase} ETH") 185 | return True 186 | 187 | logger.info(f"[{self.account_index}] Current balance: {current_balance} ETH. Waiting...") 188 | await asyncio.sleep(10) # Check every 10 seconds 189 | 190 | except Exception as e: 191 | logger.error(f"[{self.account_index}] Error checking balance: {str(e)}") 192 | await asyncio.sleep(5) 193 | 194 | logger.warning(f"[{self.account_index}] Timeout reached after {timeout} seconds. Funds not received.") 195 | return False 196 | 197 | async def withdraw(self) -> bool: 198 | """ 199 | Withdraw from exchange to the specified address with retries. 200 | Returns True if withdrawal was successful and funds arrived. 201 | """ 202 | try: 203 | if not self.config.EXCHANGES.withdrawals: 204 | raise ValueError("No withdrawal configurations found") 205 | 206 | withdrawal_config = self.config.EXCHANGES.withdrawals[0] 207 | if not withdrawal_config.networks: 208 | raise ValueError("No networks specified in withdrawal configuration") 209 | 210 | # Get chains info and validate withdrawal is enabled 211 | chains_info = await self.get_chains_info() 212 | if not chains_info: 213 | logger.error(f"[{self.account_index}] No available withdrawal networks found") 214 | return False 215 | 216 | currency = withdrawal_config.currency 217 | exchange_name = self.config.EXCHANGES.name.lower() 218 | 219 | # Get available enabled networks that match our config 220 | available_networks = [] 221 | for network in withdrawal_config.networks: 222 | mapped_network = NETWORK_MAPPINGS[exchange_name].get(network) 223 | if not mapped_network: 224 | continue 225 | 226 | # Check if network exists and is enabled in chains_info 227 | for key, info in chains_info.items(): 228 | if key == mapped_network and info["withdrawEnabled"]: 229 | available_networks.append((network, mapped_network, info)) 230 | break 231 | 232 | if not available_networks: 233 | logger.error(f"[{self.account_index}] No enabled withdrawal networks found matching configuration") 234 | return False 235 | 236 | # Randomly select from available networks 237 | network, exchange_network, network_info = random.choice(available_networks) 238 | logger.info(f"[{self.account_index}] Selected network for withdrawal: {network} ({exchange_network})") 239 | 240 | # Update web3 instance with the correct RPC URL for the selected network 241 | self.network = network 242 | rpc_url = CEX_WITHDRAWAL_RPCS.get(self.network) 243 | if not rpc_url: 244 | logger.error(f"[{self.account_index}] No RPC URL found for network: {self.network}") 245 | return False 246 | self.web3 = Web3(Web3.HTTPProvider(rpc_url)) 247 | # logger.info(f"[{self.account_index}] Updated web3 provider to: {rpc_url}") 248 | 249 | # Ensure withdrawal amount respects network minimum 250 | min_amount = max(withdrawal_config.min_amount, network_info["withdrawMin"]) 251 | max_amount = withdrawal_config.max_amount 252 | 253 | if min_amount > max_amount: 254 | logger.error(f"[{self.account_index}] Network minimum ({network_info['withdrawMin']}) is higher than configured maximum ({max_amount})") 255 | await self.exchange.close() 256 | return False 257 | 258 | amount = round(random.uniform(min_amount, max_amount), random.randint(5, 12)) 259 | 260 | # Check if we have enough balance for withdrawal 261 | if not await self.check_balance(amount): 262 | return False 263 | 264 | # Check if destination wallet balance exceeds maximum on ANY network 265 | # This prevents withdrawals if the wallet already has sufficient funds on any chain 266 | if not await self.check_all_networks_balance(withdrawal_config.max_balance): 267 | logger.warning(f"[{self.account_index}] Skipping withdrawal as destination wallet balance exceeds maximum on at least one network") 268 | await self.exchange.close() 269 | return False 270 | 271 | max_retries = withdrawal_config.retries 272 | 273 | for attempt in range(max_retries): 274 | try: 275 | # Get initial balance before withdrawal 276 | initial_balance = await self.get_eth_balance() 277 | logger.info(f"[{self.account_index}] Attempting withdrawal {attempt + 1}/{max_retries}") 278 | logger.info(f"[{self.account_index}] Withdrawing {amount} {currency} to {self.address}") 279 | 280 | # Get exchange-specific withdrawal parameters 281 | params = { 282 | 'network': exchange_network, 283 | 'fee': network_info["withdrawFee"], 284 | **EXCHANGE_PARAMS[exchange_name]["withdraw"] 285 | } 286 | 287 | withdrawal = await self.exchange.withdraw( 288 | currency, 289 | amount, 290 | self.address, 291 | params=params 292 | ) 293 | 294 | logger.success(f"[{self.account_index}] Withdrawal initiated successfully") 295 | 296 | # Wait for funds to arrive if configured 297 | if withdrawal_config.wait_for_funds: 298 | funds_received = await self.wait_for_balance_update( 299 | initial_balance, 300 | timeout=withdrawal_config.max_wait_time 301 | ) 302 | if funds_received: 303 | await self.exchange.close() 304 | return True 305 | 306 | logger.warning(f"[{self.account_index}] Funds not received yet, will retry withdrawal") 307 | else: 308 | await self.exchange.close() 309 | return True # If not waiting for funds, consider it successful 310 | 311 | except ccxt.NetworkError as e: 312 | if attempt == max_retries - 1: 313 | logger.error(f"[{self.account_index}] Network error on final attempt: {str(e)}") 314 | await self.exchange.close() 315 | raise 316 | logger.warning(f"[{self.account_index}] Network error, retrying: {str(e)}") 317 | await asyncio.sleep(5) 318 | 319 | except ccxt.ExchangeError as e: 320 | error_msg = str(e).lower() 321 | if "insufficient balance" in error_msg: 322 | logger.error(f"[{self.account_index}] Insufficient balance in exchange account") 323 | await self.exchange.close() 324 | return False 325 | if "whitelist" in error_msg or "not in withdraw whitelist" in error_msg: 326 | logger.error(f"[{self.account_index}] Address not in whitelist: {str(e)}") 327 | await self.exchange.close() 328 | return False 329 | if attempt == max_retries - 1: 330 | logger.error(f"[{self.account_index}] Exchange error on final attempt: {str(e)}") 331 | await self.exchange.close() 332 | raise 333 | logger.warning(f"[{self.account_index}] Exchange error, retrying: {str(e)}") 334 | await asyncio.sleep(5) 335 | 336 | except Exception as e: 337 | logger.error(f"[{self.account_index}] Unexpected error during withdrawal: {str(e)}") 338 | await self.exchange.close() 339 | raise 340 | 341 | logger.error(f"[{self.account_index}] Withdrawal failed after {max_retries} attempts") 342 | await self.exchange.close() 343 | return False 344 | 345 | except Exception as e: 346 | logger.error(f"[{self.account_index}] Fatal error during withdrawal process: {str(e)}") 347 | await self.exchange.close() 348 | raise 349 | 350 | async def check_all_networks_balance(self, max_balance: float) -> bool: 351 | """ 352 | Check balances on all networks in the withdrawal configuration. 353 | Returns False if any network's balance exceeds the maximum allowed. 354 | """ 355 | withdrawal_config = self.config.EXCHANGES.withdrawals[0] 356 | if not withdrawal_config.networks: 357 | raise ValueError("No networks specified in withdrawal configuration") 358 | 359 | # Store the current network and web3 instance to restore later 360 | original_network = self.network 361 | original_web3 = self.web3 362 | 363 | try: 364 | # Check balance on each network 365 | for network in withdrawal_config.networks: 366 | rpc_url = CEX_WITHDRAWAL_RPCS.get(network) 367 | if not rpc_url: 368 | logger.warning(f"[{self.account_index}] No RPC URL found for network: {network}, skipping balance check") 369 | continue 370 | 371 | # Set up web3 for this network 372 | self.network = network 373 | self.web3 = Web3(Web3.HTTPProvider(rpc_url)) 374 | 375 | try: 376 | current_balance = await self.get_eth_balance() 377 | if current_balance >= Decimal(str(max_balance)): 378 | logger.warning(f"[{self.account_index}] Destination wallet balance on {network} ({current_balance}) exceeds maximum allowed ({max_balance})") 379 | return False 380 | logger.info(f"[{self.account_index}] Balance on {network}: {current_balance} ETH (below max: {max_balance})") 381 | except Exception as e: 382 | logger.warning(f"[{self.account_index}] Error checking balance on {network}: {str(e)}") 383 | 384 | return True 385 | 386 | finally: 387 | # Restore original network and web3 instance 388 | self.network = original_network 389 | self.web3 = original_web3 --------------------------------------------------------------------------------