├── 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
--------------------------------------------------------------------------------