├── .gitignore
├── README.md
├── config
├── data
│ ├── farm.txt
│ ├── proxies.txt
│ └── register.txt
└── settings.yaml
├── console
├── __init__.py
└── main.py
├── core
├── api.py
├── auth.py
├── bot.py
├── captcha
│ └── capsolver.py
├── exceptions
│ └── base.py
└── websocket.py
├── loader.py
├── models
├── __init__.py
└── config.py
├── requirements.txt
├── run.py
└── utils
├── __init__.py
├── console.py
├── file_utils.py
├── generators.py
├── imap_utils.py
├── load_config.py
└── messages_generator.py
/.gitignore:
--------------------------------------------------------------------------------
1 | env
2 | .env
3 | __pycache__
4 | *.pyc
5 | *.pyo
6 | *.so
7 | *.egg-info
8 | *.dist-info
9 | *.egg
10 | *.egg-link
11 | *.so
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gradient Bot 🤖
2 |
3 |
4 | Gradient Bot is an advanced automation tool designed to streamline the process of account registration and farming on Gradient Network. With its powerful features and user-friendly interface, it offers a seamless experience for users looking to maximize their efficiency on the platform.
5 |
6 | ## ✨ Features
7 |
8 | - 🔐 Automatic account registration
9 | - 🧩 Integrated captcha solving
10 | - ✉️ Email verification
11 | - 🔗 Invite code binding
12 | - 🌾 Automated farming
13 | - 📊 Export statistics to CSV
14 | - 🚀 Multi-threaded registration and export support
15 |
16 | ## 🖥️ System Requirements
17 |
18 | - Windows operating system
19 | - Stable internet connection
20 | - Valid email accounts for registration
21 | - Reliable proxies (optional but recommended)
22 |
23 | ## 🛠️ Setup Guide
24 |
25 | Prepare the necessary configuration files as outlined in the next section.
26 |
27 | ## ⚙️ Configuration
28 |
29 | ### settings.yaml
30 |
31 | This file contains the general settings for the bot. Here's an example configuration:
32 |
33 | ```yaml
34 | threads: 3
35 | invite_code: "DOIFI8"
36 | capsolver_api_key: "YOUR_CAPSOLVER_API_KEY"
37 |
38 | delay_before_start:
39 | min: 5
40 | max: 10
41 |
42 | imap_settings:
43 | rambler.ru: imap.rambler.ru
44 | hotmail.com: imap-mail.outlook.com
45 | outlook.com: imap-mail.outlook.com
46 | mail.ru: imap.mail.ru
47 | gmail.com: imap.gmail.com
48 | gmx.com: imap.gmx.com
49 | yahoo.com: imap.mail.yahoo.com
50 | gmx.net: imap.gmx.net
51 | gmx.de: imap.gmx.net
52 | ```
53 |
54 | ### farm.txt
55 |
56 | List the accounts for farming, one per line:
57 |
58 | ```
59 | email1@example.com:password1
60 | email2@example.com:password2
61 | ```
62 |
63 | ### register.txt
64 |
65 | List the accounts for registration, one per line:
66 |
67 | ```
68 | newemail1@example.com:newpassword1
69 | newemail2@example.com:newpassword2
70 | ```
71 |
72 | ### proxies.txt
73 |
74 | List them in this format:
75 |
76 | ```
77 | http://user:pass@ip:port
78 | http://ip:port:user:pass
79 | http://ip:port@user:pass
80 | http://user:pass:ip:port
81 | ```
82 |
83 | ## 🚀 Usage Instructions
84 |
85 | 1. **Clone the repository**:
86 | ```sh
87 | git clone https://github.com/0ndrec/onfix_gradient.git
88 | cd onfix_gradient
89 | ```
90 | 2. **Install the required dependencies**:
91 | ```sh
92 | pip install -r requirements.txt
93 | ```
94 |
95 |
96 | ## ⚠️ Important Notes
97 |
98 | !! To successfully close a session you need to use the exit button or press ctrl + c, otherwise the session will remain active
99 |
100 | - 🖥️ The Gradient Bot has support for 2 active sessions. You can run 2 versions on one PC, or 1 version on 2 PCs.
101 | - 📧 Verify that your email providers are correctly configured in the `imap_settings` section of `settings.yaml`.
102 | - 🧩 Maintain sufficient balance in your CapSolver account for uninterrupted captcha solving.
103 |
104 | ## 🆘 Troubleshooting
105 |
106 | | Issue | Solution |
107 | |-------|----------|
108 | | Email verification failures | Check IMAP settings in `settings.yaml` and ensure they match your email provider's requirements. |
109 | | Captcha-related problems | Verify your CapSolver API key and account balance. |
110 | | Unexpected farming interruptions | Review console output for error messages and confirm account credentials. |
111 |
112 | ---
113 |
114 | 🌟 Thank you for choosing Gradient Bot! We're committed to enhancing your Gradient Network experience. 🌟
115 |
--------------------------------------------------------------------------------
/config/data/farm.txt:
--------------------------------------------------------------------------------
1 | test@gmail.com:password
--------------------------------------------------------------------------------
/config/data/proxies.txt:
--------------------------------------------------------------------------------
1 | http://proxy
--------------------------------------------------------------------------------
/config/data/register.txt:
--------------------------------------------------------------------------------
1 | test@gmail.com:password
--------------------------------------------------------------------------------
/config/settings.yaml:
--------------------------------------------------------------------------------
1 | threads: 5
2 | invite_code: "DOIFI8"
3 | capsolver_api_key: "CAP"
4 |
5 | delay_before_start:
6 | min: 5
7 | max: 10
8 |
9 | imap_settings:
10 | rambler.ru: imap.rambler.ru
11 | hotmail.com: imap-mail.outlook.com
12 | outlook.com: imap-mail.outlook.com
13 | mail.ru: imap.mail.ru
14 | gmail.com: imap.gmail.com
15 | gmx.com: imap.gmx.com
16 | yahoo.com: imap.mail.yahoo.com
17 | gmx.net: imap.gmx.net
18 | gmx.de: imap.gmx.net
19 |
--------------------------------------------------------------------------------
/console/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | from .main import Console
--------------------------------------------------------------------------------
/console/main.py:
--------------------------------------------------------------------------------
1 |
2 | import os
3 | import sys
4 | import inquirer
5 | from inquirer.themes import GreenPassion
6 | from art import text2art
7 | from colorama import Fore
8 | from loader import config
9 | from rich.console import Console as RichConsole
10 | from rich.panel import Panel
11 | from rich.table import Table
12 | from rich import box
13 | from rich.text import Text
14 | sys.path.append(os.path.realpath('.'))
15 |
16 | class Console:
17 | MODULES = ('Register', 'Farm', 'Export statistics', 'Exit')
18 | MODULES_DATA = {'Register': 'register', 'Farm': 'farm', 'Exit': 'exit', 'Export statistics': 'export_statistics'}
19 |
20 | def __init__(self):
21 | self.rich_console = RichConsole()
22 |
23 | def show_dev_info(self):
24 | os.system('cls' if os.name == 'nt' else 'clear')
25 | title = text2art('JamBit', font='small')
26 | styled_title = Text(title, style='bold cyan')
27 | version = Text('VERSION: 1.5', style='blue')
28 | dev_panel = Panel(Text.assemble(styled_title, '\n', version), border_style='yellow', expand=False, title='[bold green]Welcome[/bold green]', subtitle='[italic]Powered by Jammer[/italic]')
29 | self.rich_console.print(dev_panel)
30 | print()
31 |
32 | @staticmethod
33 | def prompt(data: list):
34 | answers = inquirer.prompt(data, theme=GreenPassion())
35 | return answers
36 |
37 | def get_module(self):
38 | questions = [inquirer.List('module', message=Fore.LIGHTBLACK_EX + 'Select the module', choices=self.MODULES)]
39 | answers = self.prompt(questions)
40 | return answers.get('module')
41 |
42 | def display_info(self):
43 | table = Table(title='Gradient Configuration', box=box.ROUNDED)
44 | table.add_column('Parameter', style='cyan')
45 | table.add_column('Value', style='magenta')
46 | table.add_row('Accounts to register', str(len(config.accounts_to_register)))
47 | table.add_row('Accounts to farm', str(len(config.accounts_to_farm)))
48 | table.add_row('Threads', str(config.threads))
49 | table.add_row('Delay before start', f'{config.delay_before_start.min} - {config.delay_before_start.max} sec')
50 | panel = Panel(table, expand=False, border_style='green', title='[bold yellow]System Information[/bold yellow]', subtitle='[italic]Use arrow keys to navigate[/italic]')
51 | self.rich_console.print(panel)
52 |
53 | def build(self) -> None:
54 | self.show_dev_info()
55 | self.display_info()
56 | module = self.get_module()
57 | config.module = self.MODULES_DATA[module]
--------------------------------------------------------------------------------
/core/api.py:
--------------------------------------------------------------------------------
1 |
2 | import asyncio
3 | import json
4 | import httpx
5 | from uuid import uuid4
6 | from typing import Literal, Tuple, Any
7 | from curl_cffi.requests import AsyncSession
8 | from models import Account
9 | from .exceptions.base import APIError
10 |
11 | class NodePayAPI:
12 | API_URL = 'https://api.gradient.network/api'
13 | GLOBAL_KEY = 'AIzaSyCWz-svq_InWzV9WaE3ez4XqxCE0C34ddI'
14 |
15 | def __init__(self, account: Account):
16 | self.account_data = account
17 | self.browser_id = str(uuid4())
18 | self.session = self.setup_session()
19 |
20 | def setup_session(self) -> AsyncSession:
21 | session = AsyncSession(impersonate='chrome124', verify=False)
22 | session.timeout = 10
23 | session.headers = {'accept': 'application/json, text/plain, */*', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'content-type': 'application/json', 'origin': 'https://app.gradient.network', 'priority': 'u=1, i', 'referer': 'https://app.gradient.network/', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'}
24 | if self.account_data.proxy:
25 | session.proxies = {'http': self.account_data.proxy.as_url, 'https': self.account_data.proxy.as_url}
26 | return session
27 |
28 | async def clear_request(self, url: str):
29 | session = AsyncSession(impersonate='chrome124', verify=False, timeout=10)
30 | session.proxies = self.session.proxies
31 | response = await session.get(url)
32 | return response
33 |
34 |
35 | async def send_request(self, request_type: Literal['POST', 'GET', 'OPTIONS']='POST', method: str=None, json_data: dict=None, params: dict=None, url: str=None, headers: dict=None, cookies: dict=None, verify: bool=True, max_retries: int=3, retry_delay: float=1.0):
36 |
37 | def verify_response(response_data: dict | list) -> dict | list:
38 | if isinstance(response_data, dict) and response_data.get('error'):
39 | raise APIError(f'API returned an error: {response_data}', response_data)
40 | if 'code' in str(response_data) and isinstance(response_data, dict) and (int(response_data.get('code')) != 200):
41 | raise APIError(f'API returned an error: {response_data}', response_data)
42 | return response_data
43 | for attempt in range(max_retries):
44 | try:
45 | if request_type == 'POST':
46 | if not url:
47 | response = await self.session.post(f'{self.API_URL}{method}', json=json_data, params=params, headers=headers if headers else self.session.headers, cookies=cookies)
48 | else:
49 | response = await self.session.post(url, json=json_data, params=params, headers=headers if headers else self.session.headers, cookies=cookies)
50 | elif request_type == 'OPTIONS':
51 | response = await self.session.options(url, headers=headers if headers else self.session.headers, cookies=cookies)
52 | elif not url:
53 | response = await self.session.get(f'{self.API_URL}{method}', params=params, headers=headers if headers else self.session.headers, cookies=cookies)
54 | else:
55 | response = await self.session.get(url, params=params, headers=headers if headers else self.session.headers, cookies=cookies)
56 | if verify:
57 | pass
58 | else:
59 | try:
60 | return verify_response(response.json())
61 | except json.JSONDecodeError:
62 | return response.text
63 | return response.text
64 | except APIError:
65 | raise
66 | except Exception as error:
67 | if attempt == max_retries - 1:
68 | raise APIError(f'Failed to send request after {max_retries} attempts: {error}')
69 | await asyncio.sleep(retry_delay)
70 | raise APIError(f'Failed to send request after {max_retries} attempts')
71 |
72 | async def user_info(self) -> dict[str, Any]:
73 | response = await self.send_request(method='/user/profile', json_data={})
74 | return response['data']
75 |
76 | async def bind_invite_code(self, invite_code: str) -> dict[str, Any]:
77 | json_data = {'code': invite_code}
78 | response = await self.send_request(method='/user/register', json_data=json_data)
79 | return response
80 |
81 | async def get_access_token(self, refresh_token: str) -> dict[str, Any]:
82 | headers = {'accept': '*/*', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'content-type': 'application/x-www-form-urlencoded', 'origin': 'https://app.gradient.network', 'priority': 'u=1, i', 'referer': 'https://app.gradient.network/', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 'x-client-data': 'CKu1yQEIh7bJAQiitskBCKmdygEIq4nLAQiSocsBCJ3+zAEIhaDNAQjArM4BCNa9zgEY69PNAQ==', 'x-client-version': 'Chrome/JsCore/10.13.0/FirebaseCore-web', 'x-firebase-gmpid': '1:236765003043:web:4300552603f2d14908a096'}
83 | data = {'grant_type': 'refresh_token', 'refresh_token': refresh_token}
84 | async with httpx.AsyncClient(headers=headers) as client:
85 | response = await client.post(url='https://securetoken.googleapis.com/v1/token', params={'key': self.GLOBAL_KEY}, data=data)
86 | response = response.json()
87 | return response
88 |
89 | async def verify_email(self, code: str) -> dict[str, Any]:
90 | json_data = {'code': code}
91 | response = await self.send_request(method='/user/verify/email', json_data=json_data)
92 | return response
93 |
94 | async def send_email_verification(self, recaptcha_token: str) -> dict[str, Any]:
95 | json_data = {'code': recaptcha_token}
96 | response = await self.send_request(method='/user/send/verify/email', json_data=json_data)
97 | return response
98 |
99 | async def lookup_sign_up(self, id_token: str):
100 | headers = {'accept': '*/*', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'content-type': 'application/json', 'origin': 'https://app.gradient.network', 'priority': 'u=1, i', 'user-agent': self.session.headers['user-agent'], 'x-client-data': 'CKu1yQEIh7bJAQiitskBCKmdygEIq4nLAQiSocsBCJ3+zAEIhaDNAQjArM4BCNa9zgEY69PNAQ==', 'x-client-version': 'Chrome/JsCore/10.13.0/FirebaseCore-web', 'x-firebase-gmpid': '1:236765003043:web:4300552603f2d14908a096'}
101 | json_data = {'idToken': id_token}
102 | response = await self.send_request(url='https://identitytoolkit.googleapis.com/v1/accounts:lookup', json_data=json_data, params={'key': self.GLOBAL_KEY}, headers=headers)
103 | return response
104 |
105 | async def sign_up(self):
106 | headers = {'accept': '*/*', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'content-type': 'application/json', 'origin': 'https://app.gradient.network', 'priority': 'u=1, i', 'user-agent': self.session.headers['user-agent'], 'x-client-data': 'CKu1yQEIh7bJAQiitskBCKmdygEIq4nLAQiSocsBCJ3+zAEIhaDNAQjArM4BCNa9zgEY69PNAQ==', 'x-client-version': 'Chrome/JsCore/10.13.0/FirebaseCore-web', 'x-firebase-gmpid': '1:236765003043:web:4300552603f2d14908a096'}
107 | json_data = {'returnSecureToken': True, 'email': self.account_data.email, 'password': self.account_data.password, 'clientType': 'CLIENT_TYPE_WEB'}
108 | response = await self.send_request(url='https://identitytoolkit.googleapis.com/v1/accounts:signUp', json_data=json_data, params={'key': self.GLOBAL_KEY}, headers=headers)
109 | return response
110 |
111 | async def nodes_list(self) -> list[dict[str, Any]]:
112 | json_data = {'page': 1, 'size': 12, 'field': 'active', 'direction': 0, 'active': '', 'banned': ''}
113 | response = await self.send_request(method='/sentrynode/list', json_data=json_data)
114 | return response['data']
115 |
116 | async def send_node_status(self):
117 | headers = {'accept': 'application/json, text/plain, */*', 'accept-language': 'en-US,en;q=0.9,ru;q=0.8', 'authorization': self.session.headers['authorization'], 'priority': 'u=1, i', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'cross-site', 'user-agent': self.session.headers['user-agent']}
118 | return await self.send_request(request_type='GET', method='/status', headers=headers)
119 |
120 | async def get_node_info(self, node_id: str) -> dict[str, Any]:
121 | response = await self.send_request(request_type='GET', method=f'/sentrynode/get/{node_id}')
122 | return response['data']
123 |
124 | async def register_node(self) -> dict[str, Any]:
125 | headers = {'accept': 'application/json, text/plain, */*', 'accept-language': 'ar-DZ,ar;q=0.9,en-US;q=0.8,en;q=0.7', 'authorization': self.session.headers['authorization'], 'content-type': 'application/x-www-form-urlencoded', 'origin': 'chrome-extension://caacbgbklghmpodbdafajbgdnegacfmo', 'priority': 'u=1, i', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'cross-site', 'user-agent': self.session.headers['user-agent']}
126 | return await self.send_request(request_type='POST', method='/sentrynode/register', headers=headers)
127 |
128 | async def sign_in(self) -> dict[str, Any]:
129 | json_data = {'returnSecureToken': True, 'email': self.account_data.email, 'password': self.account_data.password, 'clientType': 'CLIENT_TYPE_WEB'}
130 | response = await self.send_request(url='https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword', json_data=json_data, params={'key': self.GLOBAL_KEY})
131 | if response.get('idToken'):
132 | self.session.headers['authorization'] = f"Bearer {response['idToken']}"
133 | return response
134 | raise APIError(f'Failed to sign in: {response}')
--------------------------------------------------------------------------------
/core/auth.py:
--------------------------------------------------------------------------------
1 |
2 | import aiohttp
3 | import json
4 | import os
5 | from loguru import logger
6 | import inquirer
7 |
8 | class ClientAuth:
9 |
10 | def __init__(self):
11 | self.username = None
12 | self.password = None
13 | self.credentials_file = 'credentials.json'
14 | self.api_url = 'https://gradient-auth-api.onrender.com'
15 |
16 | async def authenticate_user(self, username: str, password: str) -> tuple[bool, str]:
17 | async with aiohttp.ClientSession() as session:
18 | async with session.post(f'{self.api_url}/login', json={'username': username, 'password': password}, ssl=False) as response:
19 | if response.status == 200:
20 | self.username = username
21 | self.password = password
22 | return (True, 'Login successful')
23 | else:
24 | error_details = await response.json()
25 | return (False, error_details.get('detail', 'Unknown error occurred'))
26 |
27 | async def deactivate_session(self):
28 | if self.username and self.password:
29 | async with aiohttp.ClientSession() as session:
30 | async with session.post(f'{self.api_url}/logout', json={'username': self.username, 'password': self.password}, ssl=False) as response:
31 | if response.status == 200:
32 | logger.info('>> Successfully logged out')
33 | else:
34 | error_details = await response.json()
35 | logger.error(f"Failed to logout: {error_details.get('detail', 'Unknown error')}")
36 |
37 | def save_credentials(self):
38 | credentials = {'username': self.username, 'password': self.password}
39 | try:
40 | with open(self.credentials_file, 'w') as f:
41 | json.dump(credentials, f)
42 | except IOError:
43 | logger.warning('Could not save credentials')
44 |
45 | def load_credentials(self):
46 | if os.path.exists(self.credentials_file):
47 | try:
48 | with open(self.credentials_file, 'r') as f:
49 | credentials = json.load(f)
50 | return (credentials.get('username'), credentials.get('password'))
51 | except IOError:
52 | logger.warning('Could not load credentials')
53 | return (None, None)
54 |
55 | async def login(self):
56 | saved_username, saved_password = self.load_credentials()
57 | if saved_username and saved_password:
58 | logger.info(f'Found saved credentials for user: {saved_username}')
59 | use_saved = inquirer.confirm('Do you want to use saved credentials?', default=True)
60 | if use_saved:
61 | success, message = await self.authenticate_user(saved_username, saved_password)
62 | if success:
63 | self.username = saved_username
64 | self.password = saved_password
65 | logger.info(f'User {saved_username} successfully authenticated')
66 | return True
67 | logger.error(f'Authentication failed: {message}')
68 | if message != 'Maximum number of sessions reached':
69 | os.remove(self.credentials_file)
70 | else:
71 | return False
72 | return await self.manual_login()
73 |
74 | async def manual_login(self) -> bool:
75 | questions = [inquirer.Text('username', message='Enter username'), inquirer.Password('password', message='Enter password')]
76 | answers = inquirer.prompt(questions)
77 | success, message = await self.authenticate_user(answers['username'], answers['password'])
78 | if success:
79 | self.save_credentials()
80 | logger.info(f"User {answers['username']} successfully authenticated")
81 | return True
82 | logger.error(f'Authentication failed: {message}')
83 | return False
84 |
85 | async def run(self) -> bool:
86 | if not await self.login():
87 | return False
88 | return True
--------------------------------------------------------------------------------
/core/bot.py:
--------------------------------------------------------------------------------
1 |
2 | from loader import captcha_solver, config
3 | from models import Account
4 | from utils import *
5 | from .websocket import WebSocketClient
6 | from .api import NodePayAPI
7 | from .exceptions.base import APIError
8 |
9 | class Bot(NodePayAPI):
10 | def __init__(self, account: Account):
11 | super().__init__(account)
12 |
13 | async def get_recaptcha_token(self) -> str:
14 | for _ in range(3):
15 | logger.info(f'Account: {self.account_data.email} | Solving captcha...')
16 | result, status = await captcha_solver.solve_recaptcha()
17 | if status:
18 | logger.success(f'Account: {self.account_data.email} | Captcha solved')
19 | return result
20 | logger.error(f'Account: {self.account_data.email} | {result} | Retrying...')
21 | await asyncio.sleep(3)
22 | else: # inserted
23 | raise APIError('Captcha solving failed after 3 attempts')
24 |
25 | async def process_registration(self) -> bool:
26 | try:
27 | if not await check_if_email_valid(self.account_data.imap_server, self.account_data.email, self.account_data.password):
28 | return False
29 | logger.info(f'Account: {self.account_data.email} | Registering...')
30 | tokens = await self.sign_up()
31 | await self.lookup_sign_up(tokens['idToken'])
32 | recaptcha_token = await self.get_recaptcha_token()
33 | self.session.headers['authorization'] = f"Bearer {tokens['idToken']}"
34 | response = await self.send_email_verification(recaptcha_token)
35 | if not response or response.get('code', 0)!= 200:
36 | logger.error(f'Account: {self.account_data.email} | Failed to send email verification: {response}')
37 | return False
38 | else:
39 | logger.info(f'Account: {self.account_data.email} | Email verification sent')
40 | code = await check_email_for_code(self.account_data.imap_server, self.account_data.email, self.account_data.password)
41 | if code is None:
42 | logger.error(f'Account: {self.account_data.email} | Failed to get email verification code')
43 | return False
44 | except APIError as error:
45 | if error.error_message and error.error_message.strip() == 'EMAIL_EXISTS':
46 | logger.warning(f'Account: {self.account_data.email} | Email already registered')
47 | return True
48 | logger.error(f'Account: {self.account_data.email} | Failed to register (APIError): {(error.error_message if error.error_message else str(error))}')
49 | except Exception as error:
50 | logger.error(f'Account: {self.account_data.email} | Failed to register: {error}')
51 | return False
52 | else: # inserted
53 | await self.verify_email(code)
54 | logger.success(f'Account: {self.account_data.email} | Email verified')
55 | tokens = await self.get_access_token(tokens['refreshToken'])
56 | self.session.headers['authorization'] = f"Bearer {tokens['access_token']}"
57 | await self.bind_invite_code(config.invite_code)
58 | logger.success(f'Account: {self.account_data.email} | Registered successfully')
59 | return True
60 |
61 | async def process_farming(self):
62 | try:
63 | await self.perform_farming_actions()
64 | except Exception as error:
65 | logger.error(f'Account: {self.account_data.email} | Unknown exception while farming: {error} | Stopped')
66 |
67 | async def close_session(self):
68 | try:
69 | await self.session.close()
70 | except Exception as error:
71 | logger.debug(f'Account: {self.account_data.email} | Failed to close session: {error}')
72 |
73 | async def run_websocket_client(self, node_credentials: dict):
74 | client = WebSocketClient(self.account_data)
75 | return await client.connect(client_id=node_credentials['clientid'], username=node_credentials['username'], password=node_credentials['password'])
76 |
77 | async def verify_node(self, client_id: str):
78 | pass
79 | try:
80 | pass # postinserted
81 | logger.error(f'Account: {self.account_data.email} | Node banned, stopping...')
82 | return
83 | except Exception as error:
84 | logger.error(f'Account: {self.account_data.email} | Failed to verify node: {error}')
85 | await asyncio.sleep(1800)
86 |
87 | async def process_login(self) -> bool:
88 | try:
89 | self.session = self.setup_session()
90 | logger.info(f'Account: {self.account_data.email} | Logging in...')
91 | await self.sign_in()
92 | logger.info(f'Account: {self.account_data.email} | Successfully logged in')
93 | return True
94 | except APIError as error:
95 | logger.error(f'Account: {self.account_data.email} | {error}')
96 | return False
97 |
98 | async def process_get_user_info(self) -> dict:
99 | try:
100 | if not await self.process_login():
101 | return {}
102 | user_info = await self.user_info()
103 | logger.success(f'Account: {self.account_data.email} | User info fetched')
104 | return user_info
105 | except APIError as error:
106 | logger.error(f'Account: {self.account_data.email} | {error}')
107 | return {}
108 |
109 | async def perform_farming_actions(self):
110 | if not await self.process_login():
111 | return
112 | node_credentials = await self.register_node()
113 | logger.info(f"Account: {self.account_data.email} | Node registered with ID: {node_credentials['clientid']}")
114 | websocket_task = asyncio.create_task(self.run_websocket_client(node_credentials))
115 | await asyncio.sleep(30)
116 | verify_task = asyncio.create_task(self.verify_node(node_credentials['clientid']))
117 | while True:
118 | if websocket_task.done() or websocket_task.cancelled():
119 | if not verify_task.cancelled():
120 | verify_task.cancel()
121 | if not websocket_task.cancelled():
122 | websocket_task.cancel()
123 | logger.info(f'Account: {self.account_data.email} | Retry farming in 10m')
124 | await asyncio.sleep(600)
125 | self.session = self.setup_session()
126 | return await self.perform_farming_actions()
127 | else: # inserted
128 | if verify_task.done() or verify_task.cancelled():
129 | logger.info(f'Account: {self.account_data.email} | Stopped farming')
130 | if not websocket_task.cancelled():
131 | websocket_task.cancel()
132 | if not verify_task.cancelled():
133 | verify_task.cancel()
134 | return None
135 | await asyncio.sleep(3)
--------------------------------------------------------------------------------
/core/captcha/capsolver.py:
--------------------------------------------------------------------------------
1 |
2 | import asyncio
3 | import httpx
4 | from typing import Any, Tuple
5 |
6 | class CapsolverSolver:
7 |
8 | def __init__(self, api_key: str):
9 | self.api_key = api_key
10 | self.client = httpx.AsyncClient(timeout=10)
11 | self.create_task_url = 'https://api.capsolver.com/createTask'
12 | self.get_task_result_url = 'https://api.capsolver.com/getTaskResult'
13 |
14 | async def solve_recaptcha(self) -> Tuple[str, bool]:
15 | try:
16 | captcha_data = {'clientKey': self.api_key, 'task': {'type': 'ReCaptchaV2EnterpriseTaskProxyLess', 'websiteURL': 'https://app.gradient.network/', 'websiteKey': '6Lfe5TAqAAAAAI3mJZFYU17Rzjh9DB5KDRReuqYV'}}
17 | resp = await self.client.post(self.create_task_url, json=captcha_data)
18 | if resp.status_code == 200:
19 | return await self.get_captcha_result(resp.json().get('taskId'))
20 | except httpx.RequestError as err:
21 | return (str(err), False)
22 | except Exception as err:
23 | return (str(err), False)
24 | else:
25 | return ('Failed to create captcha task', False)
26 |
27 | async def get_captcha_result(self, task_id: str) -> Tuple[Any, bool]:
28 | try:
29 | for _ in range(20):
30 | resp = await self.client.post(self.get_task_result_url, json={'clientKey': self.api_key, 'taskId': str(task_id)})
31 | if resp.status_code == 200:
32 | result = resp.json()
33 | if result.get('status') == 'failed':
34 | return (result.get('errorDescription'), False)
35 | if result.get('status') == 'ready':
36 | return (result['solution'].get('gRecaptchaResponse', ''), True)
37 | except httpx.RequestError as err:
38 | return (str(err), False)
39 | except Exception as err:
40 | return (str(err), False)
41 | else:
42 | await asyncio.sleep(5)
43 | return ('Max time for solving exhausted', False)
--------------------------------------------------------------------------------
/core/exceptions/base.py:
--------------------------------------------------------------------------------
1 |
2 | class APIError(Exception):
3 | BASE_MESSAGES = ['refresh your captcha!!', 'Incorrect answer. Try again!']
4 | pass
5 |
6 | def __init__(self, error: str, response_data: dict=None):
7 | self.error = error
8 | self.response_data = response_data
9 |
10 | @property
11 | def error_message(self) -> str:
12 | if self.response_data:
13 | try:
14 | return self.response_data['error']['message']
15 | except KeyError:
16 | return str(self.error)
17 | return None
18 |
19 | def __str__(self):
20 | return self.error
--------------------------------------------------------------------------------
/core/websocket.py:
--------------------------------------------------------------------------------
1 |
2 | import asyncio
3 | import ssl
4 | from typing import Dict
5 | import aiohttp
6 | from aiohttp import ClientSession, ClientWebSocketResponse, WSMessage, WSMsgType
7 | from loguru import logger
8 | from models import Account
9 | from utils.messages_generator import MQTTMessageGenerator
10 |
11 | class WebSocketClient:
12 | WSS_URL = 'wss://wss.gradient.network/mqtt'
13 |
14 | def __init__(self, account: Account):
15 | self.ssl_context = ssl.create_default_context()
16 | self.ssl_context.check_hostname = False
17 | self.ssl_context.verify_mode = ssl.CERT_NONE
18 | self.account_data = account
19 | self.retry_attempts = 0
20 | self.headers = {'Pragma': 'no-cache', 'Origin': 'chrome-extension://caacbgbklghmpodbdafajbgdnegacfmo', 'Accept-Language': 'en-US,en;q=0.9', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36', 'Cache-Control': 'no-cache', 'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits'}
21 |
22 | def add_retry_attempt(self) -> bool:
23 | self.retry_attempts += 1
24 | if self.retry_attempts >= 10:
25 | logger.error(f'Account: {self.account_data.email} | Too many retry attempts, stopping...')
26 | return False
27 | return True
28 |
29 | async def connect(self, client_id: str, username: str, password: str) -> None:
30 | while True:
31 | try:
32 | async with ClientSession() as session:
33 | async with session.ws_connect(self.WSS_URL, ssl=self.ssl_context, headers=self.headers, protocols=['mqtt'], proxy=self.account_data.proxy.as_url, timeout=30) as websocket:
34 | await self.handle_connection(websocket, client_id, username, password)
35 | break
36 | except aiohttp.ClientError as error:
37 | logger.error(f'Account: {self.account_data.email} | Connection error: {error}')
38 | if not self.add_retry_attempt():
39 | break
40 | except asyncio.CancelledError:
41 | logger.info(f'Account: {self.account_data.email} | Connection cancelled forced, websocket closed')
42 | return
43 | except Exception as error:
44 | logger.error(f'Account: {self.account_data.email} | Unexpected websocket error: {error}')
45 | if not self.add_retry_attempt():
46 | break
47 | logger.info(f'Account: {self.account_data.email} | Reconnecting in 5 seconds...')
48 | await asyncio.sleep(5)
49 |
50 | async def handle_connection(self, websocket: ClientWebSocketResponse, client_id: str, username: str, password: str) -> None:
51 | generator = MQTTMessageGenerator(client_id=client_id, username=username, password=password)
52 | logger.info(f'Account: {self.account_data.email} | WebSocket connection established | Client ID: {client_id} | Sending initial messages...')
53 | try:
54 | await self.send_initial_messages(websocket, generator)
55 | logger.success(f'Account: {self.account_data.email} | Initial messages sent successfully | Starting messages loop...')
56 | await self.messages_loop(websocket, generator)
57 | except Exception as e:
58 | logger.error(f'Account: {self.account_data.email} | Error while handling connection: {e}')
59 |
60 | async def send_initial_messages(self, websocket: ClientWebSocketResponse, generator: MQTTMessageGenerator) -> None:
61 | messages = [('login', generator.generate_login_message()), ('task', generator.generate_task_message()), ('ping', generator.generate_ping_message())]
62 | for msg_type, msg in messages:
63 | logger.debug(f'Account: {self.account_data.email} | Sending {msg_type} message')
64 | await websocket.send_bytes(msg)
65 | await self.handle_and_receive_message(websocket)
66 | await asyncio.sleep(2)
67 | self.retry_attempts = 0
68 |
69 | async def messages_loop(self, websocket: ClientWebSocketResponse, generator: MQTTMessageGenerator) -> None:
70 | if False:
71 | await websocket.send_bytes(generator.generate_ping_message())
72 | await self.handle_and_receive_message(websocket)
73 | await asyncio.sleep(1)
74 |
75 | async def handle_and_receive_message(self, websocket: ClientWebSocketResponse) -> None:
76 | try:
77 | msg = await websocket.receive()
78 | if isinstance(msg, WSMessage):
79 | if msg.type == WSMsgType.BINARY:
80 | return
81 | if msg.type == WSMsgType.TEXT:
82 | logger.info(f'Account: {self.account_data.email} | Received text message: {msg.data}')
83 | elif msg.type == WSMsgType.CLOSED:
84 | logger.warning(f'Account: {self.account_data.email} | WebSocket connection closed')
85 | raise asyncio.CancelledError('WebSocket closed')
86 | elif msg.type == WSMsgType.ERROR:
87 | logger.error(f'Account: {self.account_data.email} | WebSocket error: {msg.data}')
88 | raise Exception(f'WebSocket error: {msg.data}')
89 | except Exception as error:
90 | logger.error(f'Account: {self.account_data.email} | Error in handle_message: {error}')
91 |
--------------------------------------------------------------------------------
/loader.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from utils import load_config
3 | from core.captcha import capsolver
4 | config = load_config()
5 | captcha_solver = capsolver.CapsolverSolver(api_key=config.capsolver_api_key)
6 | semaphore = asyncio.Semaphore(config.threads)
--------------------------------------------------------------------------------
/models/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | from .config import *
--------------------------------------------------------------------------------
/models/config.py:
--------------------------------------------------------------------------------
1 |
2 | from better_proxy import Proxy
3 | from pydantic import BaseModel, PositiveInt, ConfigDict
4 |
5 | class Account(BaseModel):
6 | model_config = ConfigDict(arbitrary_types_allowed=True)
7 | email: str
8 | password: str
9 | imap_server: str = ''
10 | proxy: Proxy
11 |
12 | class Config(BaseModel):
13 | model_config = ConfigDict(arbitrary_types_allowed=True)
14 |
15 | class DelayBeforeStart(BaseModel):
16 | min: int
17 | max: int
18 | accounts_to_register: list[Account] = []
19 | accounts_to_farm: list[Account] = []
20 | capsolver_api_key: str
21 | invite_code: str
22 | delay_before_start: DelayBeforeStart
23 | threads: PositiveInt
24 | imap_settings: dict[str, str]
25 | module: str = ''
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aiohttp
2 | art
3 | better-proxy
4 | bs4
5 | colorama
6 | curl_cffi
7 | httpx
8 | imap_tools
9 | inquirer
10 | loguru
11 | pydantic
12 | rich
13 | urllib3
14 | pyyaml
15 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 |
2 | global active_accounts # inserted
3 | import asyncio
4 | import random
5 | import signal
6 | import sys
7 | from asyncio import Lock
8 | from loguru import logger
9 | from loader import config, semaphore
10 | from core.bot import Bot
11 | from core.auth import ClientAuth
12 | from models import Account
13 | from utils import export_results, setup, export_statistics
14 | from console import Console
15 | active_accounts = 0
16 | active_accounts_lock = Lock()
17 |
18 | async def process_registration(account: Account) -> tuple[str, str, bool]:
19 | async with semaphore:
20 | if config.delay_before_start.min > 0 and config.delay_before_start.max > 0:
21 | delay = random.randint(config.delay_before_start.min, config.delay_before_start.max)
22 | logger.info(f'Account: {account.email} | Sleeping for {delay} seconds before starting...')
23 | await asyncio.sleep(delay)
24 | bot = Bot(account)
25 | result = await bot.process_registration()
26 | await bot.close_session()
27 | return (account.email, account.password, result)
28 |
29 | async def process_farming(account: Account) -> None:
30 | global active_accounts # inserted
31 | if config.delay_before_start.min > 0 and config.delay_before_start.max > 0:
32 | delay = random.randint(config.delay_before_start.min, config.delay_before_start.max)
33 | logger.info(f'Account: {account.email} | Sleeping for {delay} seconds before starting...')
34 | await asyncio.sleep(delay)
35 | async with active_accounts_lock:
36 | active_accounts += 1
37 | logger.info(f'Active accounts: {active_accounts}')
38 | try:
39 | bot = Bot(account)
40 | await bot.process_farming()
41 | finally: # inserted
42 | async with active_accounts_lock:
43 | active_accounts -= 1
44 | logger.info(f'Active accounts: {active_accounts}')
45 |
46 | async def process_export_statistics(account: Account) -> dict:
47 | async with semaphore:
48 | if config.delay_before_start.min > 0 and config.delay_before_start.max > 0:
49 | delay = random.randint(config.delay_before_start.min, config.delay_before_start.max)
50 | logger.info(f'Account: {account.email} | Sleeping for {delay} seconds before starting...')
51 | await asyncio.sleep(delay)
52 | bot = Bot(account)
53 | user_info = await bot.process_get_user_info()
54 | await bot.close_session()
55 | return user_info
56 |
57 | async def cleanup(auth_client: ClientAuth):
58 | await auth_client.deactivate_session()
59 |
60 | async def run(auth_client):
61 | try:
62 | while True:
63 | Console().build()
64 | if config.module == 'register':
65 | if not config.accounts_to_register:
66 | logger.error('No accounts to register')
67 | else: # inserted
68 | tasks = [asyncio.create_task(process_registration(account)) for account in config.accounts_to_register]
69 | results = await asyncio.gather(*tasks)
70 | export_results(results, 'register')
71 | else: # inserted
72 | if config.module == 'farm':
73 | if not config.accounts_to_farm:
74 | logger.error('No accounts to farm')
75 | else: # inserted
76 | random.shuffle(config.accounts_to_farm)
77 | tasks = [asyncio.create_task(process_farming(account)) for account in config.accounts_to_farm]
78 | await asyncio.gather(*tasks)
79 | else: # inserted
80 | if config.module == 'export_statistics':
81 | if not config.accounts_to_farm:
82 | logger.error('No accounts to export statistics')
83 | else: # inserted
84 | tasks = [asyncio.create_task(process_export_statistics(account)) for account in config.accounts_to_farm]
85 | users_data = await asyncio.gather(*tasks)
86 | export_statistics(users_data)
87 | else: # inserted
88 | if config.module == 'exit':
89 | pass
90 | input('\n\nPress Enter to continue...')
91 | except Exception as err:
92 | logger.debug(f'An error occurred: {err}')
93 | finally: # inserted
94 | pass # postinserted
95 | await cleanup(auth_client)
96 |
97 | async def main():
98 | auth_client = ClientAuth()
99 | status = await auth_client.run()
100 | if not status:
101 | await auth_client.deactivate_session()
102 | return
103 | else: # inserted
104 | try:
105 | await run(auth_client)
106 | except asyncio.CancelledError:
107 | logger.info('Main task was cancelled')
108 | except Exception as e:
109 | logger.error(f'An error occurred: {e}')
110 | finally:
111 | await cleanup(auth_client)
112 |
113 | def handle_interrupt(signum, frame):
114 | logger.info('Received interrupt signal. Cancelling tasks...')
115 | for task in asyncio.all_tasks(loop=asyncio.get_event_loop()):
116 | try:
117 | task.cancel()
118 | except asyncio.CancelledError:
119 | continue
120 | if __name__ == '__main__':
121 | if sys.platform == 'win32':
122 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
123 | setup()
124 | signal.signal(signal.SIGINT, handle_interrupt)
125 | signal.signal(signal.SIGTERM, handle_interrupt)
126 | try:
127 | asyncio.run(main())
128 | except Exception as error:
129 | logger.debug(f'An error occurred: {error}')
130 | input('\n\nPress Enter to exit...')
--------------------------------------------------------------------------------
/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | from .load_config import load_config
3 | from .console import *
4 | from .file_utils import *
5 | from .imap_utils import *
6 | from .generators import *
--------------------------------------------------------------------------------
/utils/console.py:
--------------------------------------------------------------------------------
1 |
2 | import os
3 | import sys
4 | import urllib3
5 | from art import tprint
6 | from loguru import logger
7 |
8 | def setup():
9 | urllib3.disable_warnings()
10 | logger.remove()
11 | logger.add(sys.stdout, colorize=True, format='