├── src ├── handlers │ ├── __init__.py │ ├── error_handlers.py │ ├── payment_handlers.py │ └── subscription_handlers.py ├── public │ ├── MainMenu.PNG │ ├── ScanToken.PNG │ ├── FirstBuyers.PNG │ ├── TopHolders.PNG │ ├── DeployerWallet.PNG │ ├── HighNetWorth.PNG │ ├── MostProfitableWallet.PNG │ └── SupportedBlockchains.png ├── models │ ├── __init__.py │ ├── base_model.py │ ├── token_models.py │ ├── wallet_models.py │ ├── user_models.py │ └── watchlist_models.py ├── database │ ├── maintenance.py │ ├── base_model.py │ ├── kol_operations.py │ ├── core.py │ ├── __init__.py │ ├── user_operations.py │ ├── watchlist_operations.py │ ├── token_operations.py │ └── wallet_operations.py ├── services │ ├── __init__.py │ ├── wallet_service.py │ ├── user_service.py │ ├── subscription_service.py │ └── blockchain_service.py ├── utils │ ├── __init__.py │ ├── formatting.py │ ├── blockchain.py │ ├── user_utils.py │ └── wallet_analysis.py ├── config │ └── settings.py ├── api │ ├── client.py │ ├── token_api.py │ └── wallet_api.py ├── main.py └── test.py ├── requirements.txt ├── LICENSE ├── .env.example ├── .gitignore └── README.md /src/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/public/MainMenu.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imcrazysteven/Crypto-Trading-Analyze-Telegram-Bot/HEAD/src/public/MainMenu.PNG -------------------------------------------------------------------------------- /src/public/ScanToken.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imcrazysteven/Crypto-Trading-Analyze-Telegram-Bot/HEAD/src/public/ScanToken.PNG -------------------------------------------------------------------------------- /src/public/FirstBuyers.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imcrazysteven/Crypto-Trading-Analyze-Telegram-Bot/HEAD/src/public/FirstBuyers.PNG -------------------------------------------------------------------------------- /src/public/TopHolders.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imcrazysteven/Crypto-Trading-Analyze-Telegram-Bot/HEAD/src/public/TopHolders.PNG -------------------------------------------------------------------------------- /src/public/DeployerWallet.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imcrazysteven/Crypto-Trading-Analyze-Telegram-Bot/HEAD/src/public/DeployerWallet.PNG -------------------------------------------------------------------------------- /src/public/HighNetWorth.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imcrazysteven/Crypto-Trading-Analyze-Telegram-Bot/HEAD/src/public/HighNetWorth.PNG -------------------------------------------------------------------------------- /src/public/MostProfitableWallet.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imcrazysteven/Crypto-Trading-Analyze-Telegram-Bot/HEAD/src/public/MostProfitableWallet.PNG -------------------------------------------------------------------------------- /src/public/SupportedBlockchains.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/imcrazysteven/Crypto-Trading-Analyze-Telegram-Bot/HEAD/src/public/SupportedBlockchains.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | apscheduler 2 | python-telegram-bot 3 | python-dotenv 4 | pymongo 5 | requests 6 | web3 7 | pandas 8 | aiohttp 9 | qrcode 10 | base58 11 | solana 12 | solders 13 | anchorpy -------------------------------------------------------------------------------- /src/models/__init__.py: -------------------------------------------------------------------------------- 1 | # Export all models for easy imports 2 | from .base_model import BaseModel 3 | from .user_models import User, UserScan 4 | from .token_models import TokenData 5 | from .wallet_models import WalletData, KOLWallet 6 | from .watchlist_models import WalletWatchlist, TokenWatchlist, ApprovalWatchlist 7 | 8 | # For backward compatibility 9 | __all__ = [ 10 | 'BaseModel', 11 | 'User', 12 | 'UserScan', 13 | 'TokenData', 14 | 'WalletData', 15 | 'KOLWallet', 16 | 'WalletWatchlist', 17 | 'TokenWatchlist', 18 | 'ApprovalWatchlist' 19 | ] 20 | -------------------------------------------------------------------------------- /src/database/maintenance.py: -------------------------------------------------------------------------------- 1 | """ 2 | Database maintenance operations. 3 | """ 4 | 5 | from datetime import datetime, timedelta 6 | from .core import get_database 7 | 8 | def cleanup_old_data(days: int = 30) -> None: 9 | """ 10 | Clean up old data that hasn't been updated in a while 11 | 12 | Args: 13 | days: Number of days to keep data for 14 | """ 15 | db = get_database() 16 | cutoff_date = datetime.now() - timedelta(days=days) 17 | 18 | # Remove old token data 19 | db.token_data.delete_many({"last_updated": {"$lt": cutoff_date}}) 20 | 21 | # Remove old wallet data 22 | db.wallet_data.delete_many({"last_updated": {"$lt": cutoff_date}}) 23 | -------------------------------------------------------------------------------- /src/models/base_model.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Dict, Any, TypeVar, Type, ClassVar 3 | 4 | T = TypeVar('T', bound='BaseModel') 5 | 6 | class BaseModel: 7 | """Base model with common functionality for all models""" 8 | 9 | # Fields that should be included in to_dict 10 | _fields: ClassVar[list] = [] 11 | 12 | def to_dict(self) -> Dict[str, Any]: 13 | """Convert model object to dictionary for database storage""" 14 | result = {} 15 | for field in self._fields: 16 | value = getattr(self, field, None) 17 | result[field] = value 18 | return result 19 | 20 | @classmethod 21 | def from_dict(cls: Type[T], data: Dict[str, Any]) -> T: 22 | """Create model object from dictionary""" 23 | kwargs = {} 24 | for field in cls._fields: 25 | if field in data: 26 | kwargs[field] = data[field] 27 | return cls(**kwargs) 28 | -------------------------------------------------------------------------------- /src/services/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Services package for business logic. 3 | 4 | This package contains service modules that implement the core business logic 5 | of the application, separated from the database and API layers. 6 | """ 7 | 8 | # Import key functions for easier access 9 | from .blockchain_service import ( 10 | get_web3_provider, 11 | is_valid_address, 12 | is_valid_token_contract, 13 | is_valid_wallet_address, 14 | check_wallet_balance 15 | ) 16 | 17 | from .wallet_service import ( 18 | create_wallet, 19 | create_user_wallet, 20 | check_user_wallet_balance 21 | ) 22 | 23 | from .subscription_service import ( 24 | get_plan_payment_details, 25 | create_subscription, 26 | check_subscription_payment, 27 | get_subscription_status 28 | ) 29 | 30 | from .user_service import ( 31 | get_or_create_user, 32 | extend_premium_subscription, 33 | check_rate_limit, 34 | increment_scan_count, 35 | get_user_premium_info, 36 | get_user_usage_stats, 37 | cleanup_expired_premium_subscriptions, 38 | set_user_admin_status, 39 | get_user_count_stats 40 | ) 41 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Export all utility functions for easy imports 2 | from .user_utils import check_callback_user, check_premium_required 3 | from .token_analysis import ( 4 | handle_token_analysis_input, 5 | get_token_info, 6 | get_token_info_v2, 7 | format_first_buyers_response, 8 | format_profitable_wallets_response, 9 | format_ath_response, 10 | format_deployer_wallet_scan_response, 11 | format_top_holders_response, 12 | format_high_net_worth_holders_response 13 | ) 14 | from .wallet_analysis import ( 15 | handle_wallet_analysis_input, 16 | format_wallet_holding_duration_response, 17 | format_wallet_most_profitable_response, 18 | format_deployer_wallets_response, 19 | format_tokens_deployed_response, 20 | prompt_wallet_chain_selection, 21 | handle_wallet_holding_duration_input, 22 | handle_tokens_deployed_wallet_address_input, 23 | handle_period_selection, 24 | format_kol_wallet_profitability_response 25 | ) 26 | from .formatting import format_number, format_currency, format_percentage 27 | from .blockchain import get_explorer_url, get_chain_display_name 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Steven Leal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/database/base_model.py: -------------------------------------------------------------------------------- 1 | """ 2 | Base model class for all database models. 3 | """ 4 | 5 | from typing import Dict, Any, ClassVar, List 6 | 7 | class BaseModel: 8 | """Base model class with common functionality for all models""" 9 | 10 | # Class variable to be overridden by subclasses 11 | _fields: ClassVar[List[str]] = [] 12 | 13 | def to_dict(self) -> Dict[str, Any]: 14 | """ 15 | Convert model object to dictionary for database storage 16 | 17 | Returns: 18 | Dictionary representation of the model 19 | """ 20 | return {field: getattr(self, field) for field in self._fields if hasattr(self, field)} 21 | 22 | @classmethod 23 | def from_dict(cls, data: Dict[str, Any]) -> 'BaseModel': 24 | """ 25 | Create model object from dictionary 26 | 27 | Args: 28 | data: Dictionary containing model data 29 | 30 | Returns: 31 | Model object 32 | """ 33 | # Filter the data to only include fields defined in _fields 34 | filtered_data = {k: v for k, v in data.items() if k in cls._fields} 35 | return cls(**filtered_data) 36 | -------------------------------------------------------------------------------- /src/models/token_models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Dict, Optional, Any 3 | 4 | from .base_model import BaseModel 5 | 6 | class TokenData(BaseModel): 7 | """Model for cached token data""" 8 | 9 | _fields = [ 10 | "address", "name", "symbol", "deployer", "deployment_date", 11 | "current_price", "current_market_cap", "ath_market_cap", 12 | "ath_date", "last_updated" 13 | ] 14 | 15 | def __init__( 16 | self, 17 | address: str, 18 | name: Optional[str] = None, 19 | symbol: Optional[str] = None, 20 | deployer: Optional[str] = None, 21 | deployment_date: Optional[datetime] = None, 22 | current_price: Optional[float] = None, 23 | current_market_cap: Optional[float] = None, 24 | ath_market_cap: Optional[float] = None, 25 | ath_date: Optional[datetime] = None, 26 | last_updated: Optional[datetime] = None 27 | ): 28 | self.address = address 29 | self.name = name 30 | self.symbol = symbol 31 | self.deployer = deployer 32 | self.deployment_date = deployment_date 33 | self.current_price = current_price 34 | self.current_market_cap = current_market_cap 35 | self.ath_market_cap = ath_market_cap 36 | self.ath_date = ath_date 37 | self.last_updated = last_updated or datetime.now() 38 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Server Configuration 2 | API_SERVER_URL="http://localhost:8000" 3 | 4 | # DB Configuration 5 | MONGODB_URI="mongodb://localhost:27017/" 6 | DB_NAME="defiscope" 7 | 8 | # Ethereum Testnet (Sepolia) 9 | ETH_PROVIDER_URL=https://sepolia.infura.io/v3/ 10 | ETH_CHAIN_ID=11155111 11 | ETH_ADMIN_WALLET= 12 | 13 | # BNB Chain Testnet 14 | BNB_PROVIDER_URL=https://data-seed-prebsc-1-s1.binance.org:8545/ 15 | BNB_CHAIN_ID=97 16 | BNB_ADMIN_WALLET= 17 | 18 | DUNE_API_KEY= 19 | ZENROWS_API_KEY= 20 | 21 | # ===== PRODUCTION CONFIGURATION ===== 22 | # Uncomment these lines for production/mainnet 23 | 24 | # # Ethereum Mainnet 25 | # ETH_PROVIDER_URL=https://mainnet.infura.io/v3/ 26 | # ETH_CHAIN_ID=1 27 | # ETH_ADMIN_WALLET= 28 | 29 | # # Binance Smart Chain Mainnet 30 | # BNB_PROVIDER_URL=https://bsc-dataseed.binance.org/ 31 | # BNB_CHAIN_ID=56 32 | # BNB_ADMIN_WALLET= 33 | 34 | # Subscription Check Interval (in seconds) 35 | CHECK_INTERVAL=60 36 | 37 | # TELEGRAM BOT SETTING 38 | TELEGRAM_TOKEN= 39 | ADMIN_USER_IDS= 40 | 41 | # Free tier limits 42 | FREE_TOKEN_SCANS_DAILY=5 43 | FREE_WALLET_SCANS_DAILY=5 44 | FREE_PROFITABLE_WALLETS_LIMIT=5 45 | 46 | # Premium tier limits 47 | PREMIUM_TOKEN_SCANS_DAILY=100 48 | PREMIUM_WALLET_SCANS_DAILY=100 49 | PREMIUM_PROFITABLE_WALLETS_LIMIT=50 50 | 51 | WEB3_PROVIDER_URI=https://mainnet.infura.io/v3/ 52 | ETHERSCAN_API_KEY= 53 | 54 | -------------------------------------------------------------------------------- /src/database/kol_operations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Database operations related to KOL (Key Opinion Leader) wallets. 3 | """ 4 | 5 | from typing import List, Optional 6 | from models import KOLWallet 7 | from .core import get_database 8 | 9 | def get_kol_wallet(name_or_address: str) -> Optional[KOLWallet]: 10 | """ 11 | Get a KOL wallet by name or address 12 | 13 | Args: 14 | name_or_address: The KOL name or wallet address 15 | 16 | Returns: 17 | KOLWallet object if found, None otherwise 18 | """ 19 | db = get_database() 20 | # Try to find by name first (case-insensitive) 21 | kol = db.kol_wallets.find_one({ 22 | "$or": [ 23 | {"name": {"$regex": f"^{name_or_address}$", "$options": "i"}}, 24 | {"address": name_or_address.lower()} 25 | ] 26 | }) 27 | 28 | if kol: 29 | return KOLWallet.from_dict(kol) 30 | return None 31 | 32 | def get_all_kol_wallets() -> List[KOLWallet]: 33 | """ 34 | Get all KOL wallets 35 | 36 | Returns: 37 | List of all KOLWallet objects 38 | """ 39 | db = get_database() 40 | kols = db.kol_wallets.find().sort("name", 1) 41 | return [KOLWallet.from_dict(kol) for kol in kols] 42 | 43 | def save_kol_wallet(kol: KOLWallet) -> None: 44 | """ 45 | Save or update a KOL wallet 46 | 47 | Args: 48 | kol: The KOL wallet object to save 49 | """ 50 | db = get_database() 51 | kol_dict = kol.to_dict() 52 | kol_dict["address"] = kol_dict["address"].lower() # Normalize address 53 | 54 | db.kol_wallets.update_one( 55 | {"address": kol_dict["address"]}, 56 | {"$set": kol_dict}, 57 | upsert=True 58 | ) 59 | -------------------------------------------------------------------------------- /src/utils/formatting.py: -------------------------------------------------------------------------------- 1 | from typing import Union, Any 2 | 3 | def format_number(number=0, decimals=2): 4 | """Format a number with commas and specified decimals""" 5 | data = float(number) 6 | 7 | if data is None: 8 | return "N/A" 9 | 10 | # Handle large numbers more elegantly 11 | if data >= 1_000_000_000_000: # Trillions 12 | return f"{data / 1_000_000_000_000:.2f}T" 13 | elif data >= 1_000_000_000: # Billions 14 | return f"{data / 1_000_000_000:.2f}B" 15 | elif data >= 1_000_000: # Millions 16 | return f"{data / 1_000_000:.2f}M" 17 | elif data >= 1_000: # Thousands 18 | return f"{data / 1_000:.2f}K" 19 | else: 20 | return f"{data:,.{decimals}f}" 21 | 22 | def format_currency(amount, currency="$", decimals=2): 23 | """Format an amount as currency""" 24 | if amount is None: 25 | return "N/A" 26 | 27 | # Handle large numbers more elegantly 28 | if amount >= 1_000_000_000_000: # Trillions 29 | return f"{amount / 1_000_000_000_000:.2f}T" 30 | elif amount >= 1_000_000_000: # Billions 31 | return f"{currency}{amount / 1_000_000_000:.2f}B" 32 | elif amount >= 1_000_000: # Millions 33 | return f"{currency}{amount / 1_000_000:.2f}M" 34 | elif amount >= 1_000: # Thousands 35 | return f"{currency}{amount / 1_000:.2f}K" 36 | else: 37 | return f"{currency}{float(amount):,.{decimals}f}" 38 | 39 | def format_percentage(value: Union[int, float, Any], decimal_places: int = 2) -> str: 40 | """Format a value as a percentage with proper formatting""" 41 | if isinstance(value, (int, float)): 42 | # Check if value is already a percentage (0-100) or a decimal (0-1) 43 | if value > 0 and value < 1: 44 | value *= 100 45 | return f"{value:.{decimal_places}f}%" 46 | return str(value) 47 | -------------------------------------------------------------------------------- /src/utils/blockchain.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Optional 2 | 3 | def get_explorer_url(chain: str, address: str, is_token: bool = True) -> str: 4 | """ 5 | Get explorer URL for an address based on chain 6 | 7 | Args: 8 | chain: The blockchain network (eth, bsc, base, etc.) 9 | address: The address to view 10 | is_token: Whether the address is a token contract (True) or a wallet (False) 11 | 12 | Returns: 13 | URL to the blockchain explorer for the given address 14 | """ 15 | explorers = { 16 | 'eth': { 17 | 'token': 'https://etherscan.io/token/', 18 | 'address': 'https://etherscan.io/address/' 19 | }, 20 | 'bsc': { 21 | 'token': 'https://bscscan.com/token/', 22 | 'address': 'https://bscscan.com/address/' 23 | }, 24 | 'base': { 25 | 'token': 'https://basescan.org/token/', 26 | 'address': 'https://basescan.org/address/' 27 | }, 28 | # Add more chains as needed 29 | } 30 | 31 | chain_explorers = explorers.get(chain.lower(), explorers['eth']) 32 | base_url = chain_explorers['token'] if is_token else chain_explorers['address'] 33 | return f"{base_url}{address}" 34 | 35 | def get_chain_display_name(chain_code: str) -> str: 36 | """ 37 | Get a user-friendly display name for a chain code 38 | 39 | Args: 40 | chain_code: The blockchain network code (eth, bsc, base, etc.) 41 | 42 | Returns: 43 | User-friendly name for the chain 44 | """ 45 | chain_names = { 46 | 'eth': 'Ethereum', 47 | 'bsc': 'Binance Smart Chain', 48 | 'base': 'Base', 49 | 'arb': 'Arbitrum', 50 | 'op': 'Optimism', 51 | 'poly': 'Polygon', 52 | 'avax': 'Avalanche', 53 | 'ftm': 'Fantom' 54 | } 55 | 56 | return chain_names.get(chain_code.lower(), chain_code.upper()) 57 | -------------------------------------------------------------------------------- /src/models/wallet_models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Dict, List, Optional, Any 3 | 4 | from .base_model import BaseModel 5 | 6 | class WalletData(BaseModel): 7 | """Model for cached wallet data""" 8 | 9 | _fields = [ 10 | "address", "name", "is_kol", "is_deployer", "tokens_deployed", 11 | "avg_holding_time", "total_trades", "win_rate", "last_updated" 12 | ] 13 | 14 | def __init__( 15 | self, 16 | address: str, 17 | name: Optional[str] = None, # For KOL wallets 18 | is_kol: bool = False, 19 | is_deployer: bool = False, 20 | tokens_deployed: Optional[List[str]] = None, 21 | avg_holding_time: Optional[int] = None, # in seconds 22 | total_trades: Optional[int] = None, 23 | win_rate: Optional[float] = None, 24 | last_updated: Optional[datetime] = None 25 | ): 26 | self.address = address 27 | self.name = name 28 | self.is_kol = is_kol 29 | self.is_deployer = is_deployer 30 | self.tokens_deployed = tokens_deployed or [] 31 | self.avg_holding_time = avg_holding_time 32 | self.total_trades = total_trades 33 | self.win_rate = win_rate 34 | self.last_updated = last_updated or datetime.now() 35 | 36 | 37 | class KOLWallet(BaseModel): 38 | """Model for KOL (Key Opinion Leader) wallets""" 39 | 40 | _fields = ["address", "name", "description", "social_links", "added_at"] 41 | 42 | def __init__( 43 | self, 44 | address: str, 45 | name: str, 46 | description: Optional[str] = None, 47 | social_links: Optional[Dict[str, str]] = None, 48 | added_at: Optional[datetime] = None 49 | ): 50 | self.address = address 51 | self.name = name 52 | self.description = description 53 | self.social_links = social_links or {} 54 | self.added_at = added_at or datetime.now() 55 | -------------------------------------------------------------------------------- /src/config/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | # Load environment variables 5 | load_dotenv(override=True) 6 | 7 | API_BASE_URL = os.getenv("API_SERVER_URL", "http://localhost:8000") 8 | 9 | # Bot configuration 10 | TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN") 11 | ADMIN_USER_IDS = list(map(int, os.getenv("ADMIN_USER_IDS", "").split(","))) 12 | 13 | # Blockchain configuration 14 | WEB3_PROVIDER_URI_KEY = os.getenv("WEB3_PROVIDER_URI_KEY") 15 | ETHERSCAN_API_KEY = os.getenv("ETHERSCAN_API_KEY") 16 | BSCSCAN_API_KEY = os.getenv("BSCSCAN_API_KEY") 17 | SUBSCRIPTION_WALLET_ADDRESS=os.getenv("SUBSCRIPTION_WALLET_ADDRESS") 18 | ADMIN_WALLET_ADDRESS=os.getenv("ADMIN_WALLET_ADDRESS") 19 | ETH_PROVIDER_URL=os.getenv("ETH_PROVIDER_URL") 20 | BNB_PROVIDER_URL=os.getenv("BNB_PROVIDER_URL") 21 | BASE_PROVIDER_URL=os.getenv("BASE_PROVIDER_URL") 22 | SOL_PROVIDER_URL=os.getenv("SOL_PROVIDER_URL") 23 | SOLANA_TOKEN_PROGRAM_ID=os.getenv("SOLANA_TOKEN_PROGRAM_ID") 24 | 25 | # Database configuration 26 | MONGODB_URI = os.getenv("MONGODB_URI") 27 | DB_NAME = os.getenv("DB_NAME", "defiscope_pro_sol") 28 | 29 | # Rate limits for free users 30 | FREE_TOKEN_SCANS_DAILY=3 31 | FREE_WALLET_SCANS_DAILY=3 32 | 33 | FREE_RESPONSE_DAILY = 2 34 | PREMIUM_RESPONSE_DAILY = 10 35 | 36 | # Define subscription plans 37 | SUBSCRIPTION_PLANS = { 38 | "individual": { 39 | "weekly": { 40 | "eth": 0.1, 41 | "bnb": 0.35, 42 | "duration_days": 7 43 | }, 44 | "monthly": { 45 | "eth": 0.25, 46 | "bnb": 1.0, 47 | "duration_days": 30 48 | } 49 | }, 50 | "group_channel": { 51 | "weekly": { 52 | "eth": 0.3, 53 | "bnb": 1.0, 54 | "duration_days": 7 55 | }, 56 | "monthly": { 57 | "eth": 1.0, 58 | "bnb": 3.0, 59 | "duration_days": 30 60 | } 61 | } 62 | } 63 | 64 | # Define supported networks 65 | NETWORKS = ["ETH", "BNB", "BASE", "SOL"] 66 | -------------------------------------------------------------------------------- /src/models/user_models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Dict, Optional, Any, Literal 3 | 4 | from .base_model import BaseModel 5 | 6 | class User(BaseModel): 7 | """User model representing a bot user""" 8 | 9 | _fields = [ 10 | "user_id", "user_type", "username", 11 | "is_premium", "premium_until", "created_at", "last_active", 12 | "wallet_address", "wallet_private_key", "premium_plan", "payment_currency" 13 | ] 14 | 15 | def __init__( 16 | self, 17 | user_id: int, 18 | user_type: Literal["private", "group", "supergroup", "channel"] = "private", 19 | username: Optional[str] = None, 20 | is_premium: bool = False, 21 | premium_until: Optional[datetime] = None, 22 | created_at: Optional[datetime] = None, 23 | last_active: Optional[datetime] = None, 24 | wallet_address: Optional[str] = None, 25 | wallet_private_key: Optional[str] = None, 26 | premium_plan: Optional[str] = None, 27 | payment_currency: Optional[str] = None 28 | ): 29 | self.user_id = user_id 30 | self.user_type = user_type 31 | self.username = username 32 | self.is_premium = is_premium 33 | self.premium_until = premium_until 34 | self.created_at = created_at or datetime.now() 35 | self.last_active = last_active or datetime.now() 36 | self.wallet_address = wallet_address 37 | self.wallet_private_key = wallet_private_key 38 | self.premium_plan = premium_plan 39 | self.payment_currency = payment_currency 40 | 41 | 42 | class UserScan(BaseModel): 43 | """Model for tracking user scan usage""" 44 | 45 | _fields = ["user_id", "scan_type", "date", "count"] 46 | 47 | def __init__( 48 | self, 49 | user_id: int, 50 | scan_type: str, # 'token_scan', 'wallet_scan', etc. 51 | date: str, # ISO format date string 52 | count: int = 0 53 | ): 54 | self.user_id = user_id 55 | self.scan_type = scan_type 56 | self.date = date 57 | self.count = count 58 | -------------------------------------------------------------------------------- /src/api/client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import aiohttp 3 | import asyncio 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | class APIClient: 8 | """Client for making API requests to the token analyzer API server""" 9 | 10 | def __init__(self, timeout=600): # Default timeout of 60 seconds 11 | self._session = None 12 | self.timeout = timeout 13 | 14 | async def _get_session(self): 15 | """Get or create HTTP session""" 16 | if self._session is None or self._session.closed: 17 | self._session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.timeout)) 18 | return self._session 19 | 20 | async def close(self): 21 | """Close HTTP session""" 22 | if self._session and not self._session.closed: 23 | await self._session.close() 24 | self._session = None 25 | 26 | async def get(self, url, params=None, timeout=None): 27 | """Make a GET request to the API server""" 28 | # Use the provided timeout or fall back to the default 29 | request_timeout = timeout or self.timeout 30 | 31 | # Create a session with the specified timeout 32 | session = await self._get_session() 33 | 34 | try: 35 | # Set a timeout for this specific request 36 | timeout_ctx = aiohttp.ClientTimeout(total=request_timeout) 37 | 38 | async with session.get(url, params=params, timeout=timeout_ctx) as response: 39 | if response.status == 200: 40 | return await response.json() 41 | else: 42 | error_text = await response.text() 43 | logger.error(f"API error: {response.status} - {error_text}") 44 | return {"error": f"API error: {response.status}", "detail": error_text} 45 | except asyncio.TimeoutError: 46 | logger.error(f"Request timed out after {request_timeout} seconds for URL: {url}") 47 | return {"error": f"Request timed out after {request_timeout} seconds", "detail": "The API server took too long to respond"} 48 | except aiohttp.ClientConnectorError as e: 49 | logger.error(f"Connection error for URL {url}: {str(e)}") 50 | return {"error": f"Connection error", "detail": str(e)} 51 | except Exception as e: 52 | logger.error(f"Request error for URL {url}: {str(e)}") 53 | return {"error": f"Request failed: {str(e)}", "detail": str(e)} 54 | 55 | # Create a singleton instance 56 | api_client = APIClient() 57 | -------------------------------------------------------------------------------- /src/api/token_api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from api.client import api_client 3 | from config.settings import API_BASE_URL 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | async def fetch_token_metadata(chain, token_address): 8 | """Fetch basic token metadata""" 9 | url = f"{API_BASE_URL}/api/v1/token_meta/{chain}/{token_address}" 10 | logger.info(f"Fetching token metadata for {chain}:{token_address}") 11 | return await api_client.get(url) 12 | 13 | async def fetch_token_details(chain, token_address): 14 | """Fetch basic token details""" 15 | url = f"{API_BASE_URL}/api/v1/token_details/{chain}/{token_address}" 16 | logger.info(f"Fetching token details for {chain}:{token_address}") 17 | return await api_client.get(url) 18 | 19 | async def fetch_market_cap(chain, token_address): 20 | """Fetch token market cap data""" 21 | url = f"{API_BASE_URL}/api/v1/ath_mcap/{chain}/{token_address}" 22 | logger.info(f"Fetching market cap for {chain}:{token_address}") 23 | return await api_client.get(url) 24 | 25 | async def fetch_token_holders(chain, token_address, limit=10): 26 | """Fetch top token holders""" 27 | url = f"{API_BASE_URL}/api/v1/top_holders/{chain}/{token_address}/{limit}" 28 | logger.info(f"Fetching top {limit} holders for {chain}:{token_address}") 29 | return await api_client.get(url) 30 | 31 | async def fetch_token_security(chain, token_address): 32 | """Fetch token security information""" 33 | url = f"{API_BASE_URL}/api/v1/token_security/{chain}/{token_address}" 34 | logger.info(f"Fetching security info for {chain}:{token_address}") 35 | return await api_client.get(url) 36 | 37 | async def fetch_first_buyers(chain, token_address): 38 | """Fetch token deployer and first buyers""" 39 | url = f"{API_BASE_URL}/api/v1/first_buyers/{chain}/{token_address}" 40 | logger.info(f"Fetching first buyers for {chain}:{token_address}") 41 | return await api_client.get(url) 42 | 43 | async def fetch_token_profitable_wallets(chain, token_address): 44 | """Fetch most profitable wallets for a given token""" 45 | url = f"{API_BASE_URL}/api/v1/top_traders/{chain}/{token_address}" 46 | logger.info(f"Fetching profitable wallets for {chain}:{token_address}") 47 | return await api_client.get(url) 48 | 49 | async def fetch_token_deployer_projects(chain, token_address): 50 | """Fetch other tokens deployed by the same deployer""" 51 | url = f"{API_BASE_URL}/api/v1/token_deployer_projects/{chain}/{token_address}" 52 | logger.info(f"Fetching deployer projects for {chain}:{token_address}") 53 | return await api_client.get(url) 54 | -------------------------------------------------------------------------------- /src/models/watchlist_models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Dict, Optional, Any 3 | 4 | from .base_model import BaseModel 5 | 6 | class WalletWatchlist(BaseModel): 7 | """Model for tracking wallet activities""" 8 | 9 | _fields = ["user_id", "address", "network", "created_at", "last_checked", "is_active", "metadata"] 10 | 11 | def __init__( 12 | self, 13 | user_id: int, 14 | address: str, 15 | network: str, # 'eth', 'base', 'bsc' 16 | created_at: Optional[datetime] = None, 17 | last_checked: Optional[datetime] = None, 18 | is_active: bool = True, 19 | metadata=None 20 | ): 21 | self.user_id = user_id 22 | self.address = address 23 | self.network = network 24 | self.created_at = created_at or datetime.now() 25 | self.last_checked = last_checked or datetime.now() 26 | self.is_active = is_active 27 | self.metadata = metadata or {} 28 | 29 | 30 | class TokenWatchlist(BaseModel): 31 | """Model for tracking token activities""" 32 | 33 | _fields = ["user_id", "address", "network", "created_at", "last_checked", "is_active", "metadata"] 34 | 35 | def __init__( 36 | self, 37 | user_id: int, 38 | address: str, 39 | network: str, # 'eth', 'base', 'bsc' 40 | created_at: Optional[datetime] = None, 41 | last_checked: Optional[datetime] = None, 42 | is_active: bool = True, 43 | metadata=None 44 | ): 45 | self.user_id = user_id 46 | self.address = address 47 | self.network = network 48 | self.created_at = created_at or datetime.now() 49 | self.last_checked = last_checked or datetime.now() 50 | self.is_active = is_active 51 | self.metadata = metadata or {} 52 | 53 | 54 | class ApprovalWatchlist(BaseModel): 55 | """Model for tracking new token deployments by a wallet""" 56 | 57 | _fields = ["user_id", "address", "network", "created_at", "last_checked", "is_active", "metadata"] 58 | 59 | def __init__( 60 | self, 61 | user_id: int, 62 | address: str, 63 | network: str, # 'eth', 'base', 'bsc' 64 | created_at: Optional[datetime] = None, 65 | last_checked: Optional[datetime] = None, 66 | is_active: bool = True, 67 | metadata=None 68 | ): 69 | self.user_id = user_id 70 | self.address = address 71 | self.network = network 72 | self.created_at = created_at or datetime.now() 73 | self.last_checked = last_checked or datetime.now() 74 | self.is_active = is_active 75 | self.metadata = metadata or {} 76 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import asyncio 4 | from telegram import Update 5 | from telegram.ext import ApplicationBuilder, MessageHandler, filters 6 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 7 | from config.settings import TELEGRAM_TOKEN 8 | from handlers.error_handlers import error_handler 9 | from handlers.subscription_handlers import * 10 | from handlers.callback_handlers import ( 11 | handle_expected_input, 12 | handle_start_menu, 13 | register_command_handlers, 14 | register_callback_handlers, 15 | ) 16 | from database import init_database, cleanup_expired_premium 17 | 18 | # Configure logging 19 | logging.basicConfig( 20 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 21 | level=logging.INFO) 22 | 23 | def create_bot(): 24 | application = ApplicationBuilder().token(TELEGRAM_TOKEN).build() 25 | application.add_handler(MessageHandler(filters.Text(["/start"]), handle_start_menu)) 26 | application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_expected_input)) 27 | 28 | register_command_handlers(application) 29 | register_callback_handlers(application) 30 | register_subscription_handlers(application) 31 | 32 | # Add existing handlers 33 | application.add_error_handler(error_handler) 34 | 35 | return application 36 | 37 | async def cleanup_expired_subscriptions(): 38 | """Job to clean up expired premium subscriptions""" 39 | logging.info("Running scheduled cleanup of expired premium subscriptions") 40 | try: 41 | cleanup_expired_premium() 42 | logging.info("Expired premium subscriptions cleanup completed") 43 | except Exception as e: 44 | logging.error(f"Error during expired subscriptions cleanup: {e}") 45 | 46 | async def main_async(): 47 | # Initialize database connection 48 | if not init_database(): 49 | logging.error("Could not connect to MongoDB. Please check your configuration.") 50 | sys.exit(1) 51 | 52 | logging.info("Starting Crypto DeFi Analyze Telegram Bot...") 53 | 54 | app = create_bot() 55 | 56 | # Set up the AsyncIOScheduler 57 | scheduler = AsyncIOScheduler() 58 | scheduler.add_job( 59 | cleanup_expired_subscriptions, 60 | 'cron', 61 | hour=0, 62 | minute=0 63 | ) 64 | 65 | # Start the scheduler within the running event loop 66 | scheduler.start() 67 | 68 | # Run the application 69 | await app.initialize() 70 | await app.start() 71 | await app.updater.start_polling(allowed_updates=Update.ALL_TYPES) 72 | 73 | # Keep the application running 74 | try: 75 | await asyncio.Future() # Run forever 76 | except (KeyboardInterrupt, SystemExit): 77 | logging.info("Bot stopping...") 78 | finally: 79 | scheduler.shutdown() 80 | await app.stop() 81 | 82 | def main(): 83 | # Run the async main function with asyncio 84 | asyncio.run(main_async()) 85 | 86 | if __name__ == '__main__': 87 | main() 88 | -------------------------------------------------------------------------------- /src/api/wallet_api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from api.client import api_client 3 | from config.settings import API_BASE_URL 4 | 5 | logger = logging.getLogger(__name__) 6 | 7 | async def fetch_wallet_stats(chain, wallet_address, period="all"): 8 | """Fetch wallet trading statistics""" 9 | url = f"{API_BASE_URL}/api/v1/wallet_stat/{chain}/{wallet_address}/{period}" 10 | logger.info(f"Fetching wallet stats for {chain}:{wallet_address} period:{period}") 11 | return await api_client.get(url) 12 | 13 | async def fetch_kol_wallets(chain, order_by="pnl_1d"): 14 | """Fetch kol_wallets on a specific blockchain""" 15 | url = f"{API_BASE_URL}/api/v1/kol_wallets/{chain}/{order_by}" 16 | logger.info(f"Fetching kol_wallets for {chain}, order_by: {order_by}") 17 | return await api_client.get(url) 18 | 19 | async def fetch_wallet_holding_time(chain, wallet_address): 20 | """Fetch wallet token holding time analysis""" 21 | url = f"{API_BASE_URL}/api/v1/wallet_holding_time/{chain}/{wallet_address}" 22 | logger.info(f"Fetching wallet holding time for {chain}:{wallet_address}") 23 | return await api_client.get(url) 24 | 25 | async def fetch_wallet_deployed_tokens(chain, wallet_address): 26 | """Fetch tokens deployed by a wallet""" 27 | url = f"{API_BASE_URL}/api/v1/wallet_deployed_tokens/{chain}/{wallet_address}" 28 | logger.info(f"Fetching deployed tokens for {chain}:{wallet_address}") 29 | return await api_client.get(url) 30 | 31 | async def fetch_high_activity_wallets(chain): 32 | """Fetch high activity wallets by volume for a chain""" 33 | url = f"{API_BASE_URL}/api/v1/high_activity_wallets/{chain}" 34 | logger.info(f"Fetching high activity wallets for {chain}") 35 | return await api_client.get(url) 36 | 37 | async def fetch_high_transaction_wallets(chain): 38 | """Fetch high activity wallets by transaction count for a chain""" 39 | url = f"{API_BASE_URL}/api/v1/high_transaction_wallets/{chain}" 40 | logger.info(f"Fetching high transaction wallets for {chain}") 41 | return await api_client.get(url) 42 | 43 | async def fetch_profitable_deployers(chain): 44 | """Fetch most profitable token deployer wallets for a chain""" 45 | url = f"{API_BASE_URL}/api/v1/profitable_deployers/{chain}" 46 | logger.info(f"Fetching profitable deployers for {chain}") 47 | return await api_client.get(url) 48 | 49 | async def fetch_profitable_defi_wallets(chain): 50 | """Fetch most profitable DeFi trading wallets for a chain""" 51 | url = f"{API_BASE_URL}/api/v1/profitable_defi_wallets/{chain}" 52 | logger.info(f"Fetching profitable DeFi wallets for {chain}") 53 | return await api_client.get(url) 54 | 55 | async def fetch_high_net_worth_holders(chain, token_address, min_usd_value=10000, limit=10): 56 | """Fetch high net worth holders for a given token""" 57 | url = f"{API_BASE_URL}/api/v1/high_net_worth_holders/{chain}/{token_address}" 58 | logger.info(f"url data------> {url}") 59 | logger.info(f"Fetching high net worth holders for {chain}:{token_address} with min value: ${min_usd_value}") 60 | return await api_client.get(url) -------------------------------------------------------------------------------- /src/database/core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core database functionality for MongoDB connection and management. 3 | """ 4 | 5 | import logging 6 | from typing import Optional 7 | from pymongo import MongoClient, ASCENDING, DESCENDING 8 | from pymongo.database import Database 9 | 10 | from config.settings import MONGODB_URI, DB_NAME 11 | 12 | # Global database instance 13 | _db: Optional[Database] = None 14 | 15 | def init_database() -> bool: 16 | """ 17 | Initialize the database connection and set up indexes 18 | 19 | Returns: 20 | bool: True if initialization was successful, False otherwise 21 | """ 22 | global _db 23 | 24 | try: 25 | # Connect to MongoDB 26 | client = MongoClient(MONGODB_URI) 27 | _db = client[DB_NAME] 28 | 29 | # Set up indexes for collections 30 | # Users collection 31 | _db.users.create_index([("user_id", ASCENDING)], unique=True) 32 | 33 | # User scans collection 34 | _db.user_scans.create_index([ 35 | ("user_id", ASCENDING), 36 | ("scan_type", ASCENDING), 37 | ("date", ASCENDING) 38 | ], unique=True) 39 | 40 | # Token data collection 41 | _db.token_data.create_index([("address", ASCENDING)], unique=True) 42 | _db.token_data.create_index([("deployer", ASCENDING)]) 43 | 44 | # Wallet data collection 45 | _db.wallet_data.create_index([("address", ASCENDING)], unique=True) 46 | _db.wallet_data.create_index([("is_kol", ASCENDING)]) 47 | _db.wallet_data.create_index([("is_deployer", ASCENDING)]) 48 | 49 | # Tracking subscriptions collection 50 | _db.wallet_watchlist.create_index([ 51 | ("user_id", ASCENDING), 52 | ("address", ASCENDING), 53 | ("network", ASCENDING) 54 | ], unique=True) 55 | 56 | _db.token_watchlist.create_index([ 57 | ("user_id", ASCENDING), 58 | ("address", ASCENDING), 59 | ("network", ASCENDING) 60 | ], unique=True) 61 | 62 | _db.approval_watchlist.create_index([ 63 | ("user_id", ASCENDING), 64 | ("address", ASCENDING), 65 | ("network", ASCENDING) 66 | ], unique=True) 67 | 68 | # KOL wallets collection 69 | _db.kol_wallets.create_index([("address", ASCENDING)], unique=True) 70 | _db.kol_wallets.create_index([("name", ASCENDING)]) 71 | 72 | server_info = client.server_info() 73 | logging.info(f"Successfully connected to MongoDB version: {server_info.get('version')}") 74 | logging.info(f"Using database: {DB_NAME}") 75 | return True 76 | except Exception as e: 77 | logging.error(f"Failed to initialize database: {e}") 78 | return False 79 | 80 | def get_database() -> Database: 81 | """ 82 | Get the database instance 83 | 84 | Returns: 85 | Database: The MongoDB database instance 86 | """ 87 | global _db 88 | if _db is None: 89 | init_database() 90 | return _db 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Virtual Environment 2 | venv/ 3 | myenv/ 4 | env/ 5 | ENV/ 6 | .env/ 7 | .venv/ 8 | virtualenv/ 9 | *env/ 10 | Scripts/ 11 | Lib/ 12 | Include/ 13 | pyvenv.cfg 14 | 15 | # Python bytecode 16 | __pycache__/ 17 | *.py[cod] 18 | *$py.class 19 | 20 | # Logs 21 | logs 22 | *.log 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | lerna-debug.log* 27 | .pnpm-debug.log* 28 | 29 | # Diagnostic reports (https://nodejs.org/api/report.html) 30 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 31 | 32 | # Runtime data 33 | pids 34 | *.pid 35 | *.seed 36 | *.pid.lock 37 | 38 | # Directory for instrumented libs generated by jscoverage/JSCover 39 | lib-cov 40 | 41 | # Coverage directory used by tools like istanbul 42 | coverage 43 | *.lcov 44 | 45 | # nyc test coverage 46 | .nyc_output 47 | 48 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 49 | .grunt 50 | 51 | # Bower dependency directory (https://bower.io/) 52 | bower_components 53 | 54 | # node-waf configuration 55 | .lock-wscript 56 | 57 | # Compiled binary addons (https://nodejs.org/api/addons.html) 58 | build/Release 59 | 60 | # Dependency directories 61 | node_modules/ 62 | jspm_packages/ 63 | 64 | # Snowpack dependency directory (https://snowpack.dev/) 65 | web_modules/ 66 | 67 | # TypeScript cache 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | .npm 72 | 73 | # Optional eslint cache 74 | .eslintcache 75 | 76 | # Optional stylelint cache 77 | .stylelintcache 78 | 79 | # Microbundle cache 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | .node_repl_history 87 | 88 | # Output of 'npm pack' 89 | *.tgz 90 | 91 | # Yarn Integrity file 92 | .yarn-integrity 93 | 94 | # dotenv environment variable files 95 | .env 96 | .env.development.local 97 | .env.test.local 98 | .env.production.local 99 | .env.local 100 | 101 | # parcel-bundler cache (https://parceljs.org/) 102 | .cache 103 | .parcel-cache 104 | 105 | # Next.js build output 106 | .next 107 | out 108 | 109 | # Nuxt.js build / generate output 110 | .nuxt 111 | dist 112 | 113 | # Gatsby files 114 | .cache/ 115 | # Comment in the public line in if your project uses Gatsby and not Next.js 116 | # https://nextjs.org/blog/next-9-1#public-directory-support 117 | # public 118 | 119 | # vuepress build output 120 | .vuepress/dist 121 | 122 | # vuepress v2.x temp and cache directory 123 | .temp 124 | .cache 125 | 126 | # vitepress build output 127 | **/.vitepress/dist 128 | 129 | # vitepress cache directory 130 | **/.vitepress/cache 131 | 132 | # Docusaurus cache and generated files 133 | .docusaurus 134 | 135 | # Serverless directories 136 | .serverless/ 137 | 138 | # FuseBox cache 139 | .fusebox/ 140 | 141 | # DynamoDB Local files 142 | .dynamodb/ 143 | 144 | # TernJS port file 145 | .tern-port 146 | 147 | # Stores VSCode versions used for testing VSCode extensions 148 | .vscode-test 149 | 150 | # yarn v2 151 | .yarn/cache 152 | .yarn/unplugged 153 | .yarn/build-state.yml 154 | .yarn/install-state.gz 155 | .pnp.* 156 | -------------------------------------------------------------------------------- /src/utils/user_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional 3 | 4 | from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup 5 | from telegram.ext import ContextTypes 6 | from telegram.constants import ParseMode 7 | 8 | from models import User 9 | from config.settings import FREE_WALLET_SCANS_DAILY 10 | from services.user_service import get_or_create_user, check_rate_limit 11 | 12 | async def check_callback_user(update: Update) -> User: 13 | """Check if user exists in database, create if not, and update activity""" 14 | return await get_or_create_user( 15 | user_id=update.effective_chat.id, 16 | user_type=update.effective_chat.type, 17 | username=update.effective_user.username if update.effective_chat.type == "private" else update.effective_chat.title, 18 | ) 19 | 20 | async def check_premium_required(update, feature_name, daily_limit=None): 21 | """ 22 | Check if premium is required for a feature based on user's daily usage 23 | Returns True if premium is required, False otherwise 24 | """ 25 | user = await check_callback_user(update) 26 | 27 | # If user is premium, they can always use the feature 28 | if user.is_premium: 29 | return False 30 | 31 | # If no daily limit is specified, premium is required 32 | if daily_limit is None: 33 | # Use callback_query for inline button clicks, or message for direct commands 34 | if update.callback_query: 35 | await update.callback_query.edit_message_text( 36 | "⭐ Premium Feature\n\n" 37 | "This feature is only available to premium users.\n" 38 | "💎 Upgrade to Premium to access this feature.\n" 39 | "✨ To get premium, use /premium command.", 40 | parse_mode=ParseMode.HTML 41 | ) 42 | elif update.message: 43 | await update.message.reply_text( 44 | "⭐ Premium Feature\n\n" 45 | "This feature is only available to premium users.\n" 46 | "💎 Upgrade to Premium to access this feature.\n" 47 | "✨ To get premium, use /premium command.", 48 | parse_mode=ParseMode.HTML 49 | ) 50 | return True 51 | 52 | # Check if user has reached daily limit 53 | has_reached_limit, current_count = await check_rate_limit( 54 | user.user_id, feature_name, daily_limit 55 | ) 56 | 57 | if has_reached_limit: 58 | # Use callback_query for inline button clicks, or message for direct commands 59 | if update.callback_query: 60 | await update.callback_query.edit_message_text( 61 | "⚠️ Daily Limit Reached\n\n" 62 | f"You've already used {current_count} out of your {daily_limit} free daily scans.\n" 63 | "💎 Upgrade to Premium for unlimited scans.\n" 64 | "✨ To get premium, use /premium command.", 65 | parse_mode=ParseMode.HTML 66 | ) 67 | elif update.message: 68 | await update.message.reply_text( 69 | "⚠️ Daily Limit Reached\n\n" 70 | f"You've already used {current_count} out of your {daily_limit} free daily scans.\n" 71 | "💎 Upgrade to Premium for unlimited scans.\n" 72 | "✨ To get premium, use /premium command.", 73 | parse_mode=ParseMode.HTML 74 | ) 75 | return True 76 | 77 | return False 78 | -------------------------------------------------------------------------------- /src/database/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Database package for handling MongoDB operations. 3 | This package provides functions for interacting with the database. 4 | """ 5 | 6 | # Import core database functions 7 | from .core import init_database, get_database 8 | 9 | # Import user-related functions 10 | from .user_operations import ( 11 | get_user, save_user, update_user_activity, set_premium_status, 12 | get_user_scan_count, increment_user_scan_count, reset_user_scan_counts, 13 | update_user_premium_status, get_users_with_expiring_premium, 14 | get_all_users, get_admin_users, set_user_admin_status, get_user_counts, 15 | cleanup_expired_premium 16 | ) 17 | 18 | # Import token-related functions 19 | from .token_operations import ( 20 | get_tokendata, save_token_data, get_tokens_by_deployer, 21 | get_token_first_buyers, get_token_profitable_wallets, get_ath_data, 22 | get_deployer_wallet_scan_data, get_token_top_holders, get_high_net_worth_holders 23 | ) 24 | 25 | # Import wallet-related functions 26 | from .wallet_operations import ( 27 | save_wallet_data, get_profitable_wallets, get_profitable_deployers, 28 | get_wallet_holding_duration, get_tokens_deployed_by_wallet, 29 | get_wallet_most_profitable_in_period, get_most_profitable_token_deployer_wallets, 30 | get_kol_wallet_profitability 31 | ) 32 | 33 | # Import watchlist-related functions 34 | from .watchlist_operations import ( 35 | get_user_wallet_watchlist, get_wallet_watchlist_entry, save_wallet_watchlist_entry, delete_wallet_watchlist_entry, 36 | get_user_token_watchlist, get_token_watchlist_entry, save_token_watchlist_entry, delete_token_watchlist_entry, 37 | get_user_approval_watchlist, get_approval_watchlist_entry, save_approval_watchlist_entry, delete_approval_watchlist_entry, 38 | delete_tracking_subscription, update_subscription_check_time, 39 | get_all_active_wallet_watchlist, get_all_active_token_watchlist, get_all_active_approval_watchlist 40 | ) 41 | 42 | # Import KOL-related functions 43 | from .kol_operations import ( 44 | get_kol_wallet, get_all_kol_wallets, save_kol_wallet 45 | ) 46 | 47 | # Import maintenance functions 48 | from .maintenance import ( 49 | cleanup_old_data 50 | ) 51 | 52 | # For backward compatibility, expose all functions at the package level 53 | __all__ = [ 54 | # Core 55 | 'init_database', 'get_database', 56 | 57 | # User operations 58 | 'get_user', 'save_user', 'update_user_activity', 'set_premium_status', 59 | 'get_user_scan_count', 'increment_user_scan_count', 'reset_user_scan_counts', 60 | 'update_user_premium_status', 'get_users_with_expiring_premium', 61 | 'get_all_users', 'get_admin_users', 'set_user_admin_status', 'get_user_counts', 62 | 'cleanup_expired_premium', 63 | 64 | # Token operations 65 | 'get_tokendata', 'save_token_data', 'get_tokens_by_deployer', 66 | 'get_token_first_buyers', 'get_token_profitable_wallets', 'get_ath_data', 67 | 'get_deployer_wallet_scan_data', 'get_token_top_holders', 'get_high_net_worth_holders', 68 | 69 | # Wallet operations 70 | 'save_wallet_data', 'get_profitable_wallets', 'get_profitable_deployers', 71 | 'get_wallet_holding_duration', 'get_tokens_deployed_by_wallet', 72 | 'get_wallet_most_profitable_in_period', 'get_most_profitable_token_deployer_wallets', 73 | 'get_kol_wallet_profitability', 74 | 75 | # Watchlist operations 76 | 'get_user_wallet_watchlist', 'get_wallet_watchlist_entry', 'save_wallet_watchlist_entry', 'delete_wallet_watchlist_entry', 77 | 'get_user_token_watchlist', 'get_token_watchlist_entry', 'save_token_watchlist_entry', 'delete_token_watchlist_entry', 78 | 'get_user_approval_watchlist', 'get_approval_watchlist_entry', 'save_approval_watchlist_entry', 'delete_approval_watchlist_entry', 79 | 'delete_tracking_subscription', 'update_subscription_check_time', 80 | 'get_all_active_wallet_watchlist', 'get_all_active_token_watchlist', 'get_all_active_approval_watchlist', 81 | 82 | # KOL operations 83 | 'get_kol_wallet', 'get_all_kol_wallets', 'save_kol_wallet', 84 | 85 | # Maintenance 86 | 'cleanup_old_data' 87 | ] 88 | -------------------------------------------------------------------------------- /src/services/wallet_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | Wallet service for creating and managing blockchain wallets. 3 | """ 4 | 5 | import logging 6 | import time 7 | from typing import Dict, Any, Optional 8 | from datetime import datetime 9 | from web3 import Web3 10 | 11 | from services.blockchain_service import get_web3_provider, check_wallet_balance 12 | 13 | def create_wallet(network: str) -> Dict[str, Any]: 14 | """ 15 | Create a new wallet on the specified network. 16 | 17 | Args: 18 | network: The blockchain network (ETH, BSC, BASE) 19 | 20 | Returns: 21 | Dictionary with wallet information or error details 22 | """ 23 | try: 24 | web3 = get_web3_provider(network) 25 | account = web3.eth.account.create() 26 | 27 | wallet_info = { 28 | "address": account.address, 29 | "private_key": account.key.hex(), 30 | "network": network 31 | } 32 | 33 | logging.info(f"Created new {network} wallet: {account.address}") 34 | return { 35 | "success": True, 36 | "data": wallet_info 37 | } 38 | except Exception as e: 39 | logging.error(f"Failed to create {network} wallet: {str(e)}") 40 | return { 41 | "success": False, 42 | "error": { 43 | "code": "WALLET_CREATION_FAILED", 44 | "message": f"Failed to create new {network} wallet", 45 | "details": str(e) 46 | } 47 | } 48 | 49 | async def create_user_wallet(user_id: int, network: str) -> Dict[str, Any]: 50 | """ 51 | Create a wallet for a user on the specified network 52 | 53 | Args: 54 | user_id: The user's ID 55 | network: The network to create the wallet on (ETH or BNB) 56 | 57 | Returns: 58 | Dictionary with wallet information or error details 59 | """ 60 | from database import get_user, save_user 61 | 62 | try: 63 | # Get user from database 64 | user = get_user(user_id) 65 | if not user: 66 | return { 67 | "success": False, 68 | "error": "User not found" 69 | } 70 | 71 | # Check if user already has a wallet 72 | if user.wallet_address and user.wallet_private_key: 73 | return { 74 | "success": True, 75 | "data": { 76 | "wallet_address": user.wallet_address, 77 | "message": "User already has a wallet" 78 | } 79 | } 80 | 81 | # Create wallet 82 | wallet_result = create_wallet(network) 83 | 84 | if not wallet_result["success"]: 85 | return wallet_result 86 | 87 | wallet_info = wallet_result["data"] 88 | 89 | # Update user with wallet information 90 | user.wallet_address = wallet_info["address"] 91 | user.wallet_private_key = wallet_info["private_key"] 92 | user.payment_currency = network 93 | 94 | # Save updated user to database 95 | save_user(user) 96 | 97 | logging.info(f"Created {network} wallet for user {user_id}: {wallet_info['address']}") 98 | 99 | return { 100 | "success": True, 101 | "data": { 102 | "wallet_address": wallet_info["address"], 103 | "network": network 104 | } 105 | } 106 | except Exception as e: 107 | logging.error(f"Error creating wallet for user {user_id}: {str(e)}") 108 | return { 109 | "success": False, 110 | "error": f"Failed to create wallet: {str(e)}" 111 | } 112 | 113 | async def check_user_wallet_balance(user_id: int) -> Dict[str, Any]: 114 | """ 115 | Check the balance of a user's wallet 116 | 117 | Args: 118 | user_id: The user's ID 119 | 120 | Returns: 121 | Dictionary with balance information or error details 122 | """ 123 | from database import get_user 124 | 125 | try: 126 | # Get user from database 127 | user = get_user(user_id) 128 | if not user: 129 | return { 130 | "success": False, 131 | "error": "User not found" 132 | } 133 | 134 | # Check if user has a wallet 135 | if not user.wallet_address or not user.payment_currency: 136 | return { 137 | "success": False, 138 | "error": "User does not have a wallet" 139 | } 140 | 141 | # Check wallet balance 142 | return check_wallet_balance( 143 | user.wallet_address, 144 | user.payment_currency 145 | ) 146 | except Exception as e: 147 | logging.error(f"Error checking wallet balance for user {user_id}: {str(e)}") 148 | return { 149 | "success": False, 150 | "error": f"Failed to check wallet balance: {str(e)}" 151 | } 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crypto Trading Analyze Telegram Bot 2 | 3 | A cutting-edge Telegram bot designed to empower users in the decentralized finance (DeFi) space. Analyze tokens, track wallets, and uncover profitable opportunities with advanced analytics and real-time insights. 4 | 5 | --- 6 | 7 | ## 🔎 **Your Ultimate DeFi Intelligence Bot!** 8 | Stay ahead in the crypto game with powerful analytics, wallet tracking, and market insights. 📊💰 9 | 10 |

11 | Main Menu 12 |

13 | 14 | --- 15 | 16 | ## ✨ **Features Overview** 17 | 18 | ### 😍 Supported Blockchains 19 | 20 |

21 | Supported Blockchains 22 |

23 | 24 | - **EVM-based chains:** Ethereum, Binance Smart Chain (BSC), Base 25 | - **Solana:** A high-performance blockchain for decentralized applications. 26 | 27 | --- 28 | 29 | ### 📊 **Token Analysis** 30 | 31 |

32 | Scan Token 33 |

34 | 35 | - **First Buyers & Profits of a Token:** 36 | Identify the first 1-50 buy wallets of a token, including buy/sell amounts, trades, profit/loss (PNL), and win rates. 37 | *(Free users: 3 token scans daily | Premium: Unlimited)* 38 | 39 |

40 | First Buyers 41 |

42 | 43 | - **Most Profitable Wallets of a Token:** 44 | Discover wallets with the highest profits in any token. 45 | *(Free users: 3 token scans daily | Premium: Unlimited)* 46 | 47 |

48 | Most Profitable Wallets 49 |

50 | 51 | - **Market Cap & ATH:** 52 | View all-time high (ATH) market cap, date, and percentage drop from ATH. 53 | *(Free users: 3 token scans daily | Premium: Unlimited)* 54 | 55 | - **Deployer Wallet Scan:** *(Premium)* 56 | Analyze deployer wallets to see other tokens deployed, their ATH market cap, and ROI. 57 | 58 |

59 | Deployer Wallet Scan 60 |

61 | 62 | - **Top Holders & Whale Watch:** *(Premium)* 63 | Monitor the top 10 holders and whale wallets of any token. 64 | 65 |

66 | Top Holders 67 |

68 | 69 | - **High Net Worth Wallet Holders:** *(Premium)* 70 | Track wallets with $10,000+ holdings, including total USD worth, token list, and holding durations. 71 | 72 |

73 | High Net Worth Wallet Holders 74 |

75 | 76 | --- 77 | 78 | ### 🕵️ **Wallet Analysis** 79 | 80 | 83 | 84 | - **Most Profitable Wallets in a Time Period:** 85 | Identify wallets with the highest profits over 1-30 days. 86 | *(Free: 2 wallets per query | Premium: Unlimited)* 87 | 88 | - **Wallet Holding Duration:** 89 | Analyze how long wallets hold tokens before selling. 90 | *(Free: 3 scans daily | Premium: Unlimited)* 91 | 92 | - **Most Profitable Token Deployer Wallets:** 93 | Spot high-profit deployers over 1-30 days. 94 | *(Free: 2 deployers per query | Premium: Unlimited)* 95 | 96 | - **Tokens Deployed by Wallet:** *(Premium)* 97 | View tokens deployed by any wallet, including name, ticker, current price, deployment date, market cap, and ATH. 98 | 99 | --- 100 | 101 | ### 🔔 **Tracking & Monitoring** *(Premium)* 102 | 103 | 106 | 107 | - **Track Buy/Sell Activity:** 108 | Receive alerts when a wallet buys or sells any token. 109 | 110 | - **Track New Token Deployments:** 111 | Get notified when a wallet (or its linked wallets) deploy a new token. 112 | 113 | - **Profitable Wallets in a Token:** 114 | Monitor the most profitable wallets in any token within 1-30 days. 115 | 116 | --- 117 | 118 | ### 🐳 **KOL Wallets** 119 | 120 | 123 | 124 | - **KOL Wallets Profitability:** 125 | Track the performance and profit/loss (PNL) of top Key Opinion Leader (KOL) wallets over 1-30 days. 126 | *(Free: 3 scans daily | Premium: Unlimited)* 127 | 128 | - **Track Whale Wallets:** *(Premium)* 129 | Get notified when developers, top holders, or whales sell a token. 130 | 131 | --- 132 | 133 | ### Premium Features 134 | 135 | Upgrade to premium to unlock: 136 | 137 | - Unlimited token and wallet scans 138 | - Access to deployer wallet analysis 139 | - Tracking tokens, wallets, and deployers 140 | - View top holders of any token 141 | - Access to profitable wallets database 142 | - High net worth wallet monitoring 143 | - Priority support 144 | 145 | --- 146 | 147 | ## Setup and Installation 148 | 149 | 1. Clone the repository: 150 | ```bash 151 | git clone https://github.com/imcrazysteven/Crypto-Trading-Analyze-Telegram-Bot.git 152 | cd Crypto-Trading-Analyze-Telegram-Bot 153 | ``` 154 | 155 | 2. Install dependencies: 156 | 157 | ```bash 158 | pip install -r requirements.txt 159 | ``` 160 | 161 | 3. Set up environment variables: 162 | 163 | Copy the `.env.example` file to `.env` and update the placeholders with your actual values: 164 | 165 | ```bash 166 | cp .env.example .env 167 | ``` 168 | 169 | Edit the `.env` file with your configuration: 170 | 171 | ```properties 172 | API_SERVER_URL="http://localhost:8000" 173 | MONGODB_URI="mongodb://localhost:27017/" 174 | DB_NAME="defiscope" 175 | ETH_PROVIDER_URL="https://sepolia.infura.io/v3/" 176 | ETH_CHAIN_ID=11155111 177 | ETH_ADMIN_WALLET="" 178 | BNB_PROVIDER_URL="https://data-seed-prebsc-1-s1.binance.org:8545/" 179 | BNB_CHAIN_ID=97 180 | BNB_ADMIN_WALLET="" 181 | TELEGRAM_TOKEN="" 182 | ADMIN_USER_IDS="" 183 | WEB3_PROVIDER_URI="https://mainnet.infura.io/v3/" 184 | ETHERSCAN_API_KEY="" 185 | DUNE_API_KEY="" 186 | ZENROWS_API_KEY="" 187 | ``` 188 | 189 | 4. Run the bot: 190 | ```bash 191 | python src/main.py 192 | ``` 193 | 194 | --- 195 | 196 | ## License 197 | This project is licensed under the [MIT License](./LICENSE) - see the LICENSE file for details. 198 | 199 | --- 200 | 201 | 202 | ## 🤝 Contributing 203 | 204 | We welcome contributions! Please feel free to submit issues, feature requests, or pull requests. 205 | 206 | ## 📞 Contact & Support 207 | 208 | APIs are in private now. For inquiries or support, please contact me via the following information. 209 | 210 | 211 | - **Email**: [imcrazysteven143@gmail.com](mailto:imcrazysteven143@gmail.com) 212 | - **GitHub**: [Steven (@imcrazysteven)](https://github.com/imcrazysteven) 213 | - **Telegram**: [@imcrazysteven](https://t.me/imcrazysteven) 214 | - **Twitter**: [@imcrazysteven](https://x.com/imcrazysteven) 215 | - **Instagram**: [@imcrazysteven](https://www.instagram.com/imcrazysteven/) 216 | 217 | 218 | --- 219 | 220 | **⚠️ Disclaimer**: This project is for educational and research purposes. Cryptocurrency trading involves substantial risk. Always conduct thorough research and consider consulting with financial advisors before making investment decisions. If you want to have your own sophisticated copy trading bot, please contact me through contact information. 221 | -------------------------------------------------------------------------------- /src/handlers/error_handlers.py: -------------------------------------------------------------------------------- 1 | import html 2 | import json 3 | import logging 4 | import traceback 5 | from typing import Optional 6 | 7 | from telegram import Update 8 | from telegram.constants import ParseMode 9 | from telegram.error import ( 10 | BadRequest, ChatMigrated, Conflict, Forbidden, InvalidToken, NetworkError, 11 | RetryAfter, TelegramError, TimedOut 12 | ) 13 | from telegram.ext import ContextTypes 14 | 15 | # Enable logging 16 | logging.basicConfig( 17 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 18 | level=logging.INFO 19 | ) 20 | logger = logging.getLogger(__name__) 21 | 22 | DEVELOPER_CHAT_ID = None # Replace with your Telegram ID for error reporting 23 | 24 | async def error_handler(update: Optional[Update], context: ContextTypes.DEFAULT_TYPE) -> None: 25 | """Handle errors raised during updates processing.""" 26 | # Log the error 27 | logger.error("Exception while handling an update:", exc_info=context.error) 28 | 29 | # Get the error traceback 30 | tb_list = traceback.format_exception(None, context.error, context.error.__traceback__) 31 | tb_string = "".join(tb_list) 32 | 33 | # Build the error message 34 | error_message = ( 35 | f"An exception occurred while processing an update:\n\n" 36 | f"
{html.escape(tb_string)}
" 37 | ) 38 | 39 | # Extract update data for error reporting 40 | update_str = update.to_dict() if update else "No update" 41 | 42 | # Truncate the update string if it's too long 43 | if isinstance(update_str, str): 44 | if len(update_str) > 1000: 45 | update_str = update_str[:1000] + "..." 46 | else: 47 | update_str = json.dumps(update_str, indent=2, ensure_ascii=False) 48 | if len(update_str) > 1000: 49 | update_str = json.dumps(update_str, ensure_ascii=False)[:1000] + "..." 50 | 51 | # Add update data to error message 52 | error_message += f"\n\nUpdate:
{html.escape(str(update_str))}
" 53 | 54 | # Handle specific errors 55 | if isinstance(context.error, BadRequest): 56 | await handle_bad_request_error(update, context) 57 | elif isinstance(context.error, TimedOut): 58 | await handle_timeout_error(update, context) 59 | elif isinstance(context.error, NetworkError): 60 | await handle_network_error(update, context) 61 | elif isinstance(context.error, Forbidden): 62 | await handle_forbidden_error(update, context) 63 | elif isinstance(context.error, RetryAfter): 64 | await handle_retry_after_error(update, context) 65 | elif isinstance(context.error, Conflict): 66 | await handle_conflict_error(update, context) 67 | elif isinstance(context.error, ChatMigrated): 68 | await handle_chat_migrated_error(update, context) 69 | elif isinstance(context.error, InvalidToken): 70 | await handle_invalid_token_error(update, context) 71 | elif isinstance(context.error, TelegramError): 72 | await handle_telegram_error(update, context) 73 | 74 | # Send error message to developer if developer chat ID is set 75 | if DEVELOPER_CHAT_ID: 76 | # Split the message if it's too long 77 | if len(error_message) > 4000: 78 | parts = [error_message[i:i+4000] for i in range(0, len(error_message), 4000)] 79 | for part in parts: 80 | await context.bot.send_message( 81 | chat_id=DEVELOPER_CHAT_ID, 82 | text=part, 83 | parse_mode=ParseMode.HTML 84 | ) 85 | else: 86 | await context.bot.send_message( 87 | chat_id=DEVELOPER_CHAT_ID, 88 | text=error_message, 89 | parse_mode=ParseMode.HTML 90 | ) 91 | 92 | async def handle_bad_request_error(update: Optional[Update], context: ContextTypes.DEFAULT_TYPE) -> None: 93 | """Handle BadRequest errors.""" 94 | if update and update.effective_message: 95 | error_message = "Sorry, I couldn't process that request. Please try again." 96 | 97 | try: 98 | await update.effective_message.reply_text(error_message) 99 | except Exception as e: 100 | logger.error(f"Failed to send error message: {e}") 101 | 102 | async def handle_timeout_error(update: Optional[Update], context: ContextTypes.DEFAULT_TYPE) -> None: 103 | """Handle TimedOut errors.""" 104 | if update and update.effective_message: 105 | error_message = "The request timed out. Please try again later." 106 | 107 | try: 108 | await update.effective_message.reply_text(error_message) 109 | except Exception as e: 110 | logger.error(f"Failed to send error message: {e}") 111 | 112 | async def handle_network_error(update: Optional[Update], context: ContextTypes.DEFAULT_TYPE) -> None: 113 | """Handle NetworkError errors.""" 114 | if update and update.effective_message: 115 | error_message = "A network error occurred. Please try again later." 116 | 117 | try: 118 | await update.effective_message.reply_text(error_message) 119 | except Exception as e: 120 | logger.error(f"Failed to send error message: {e}") 121 | 122 | async def handle_forbidden_error(update: Optional[Update], context: ContextTypes.DEFAULT_TYPE) -> None: 123 | """Handle Forbidden errors.""" 124 | if update and update.effective_message: 125 | error_message = "I don't have permission to perform this action." 126 | 127 | try: 128 | await update.effective_message.reply_text(error_message) 129 | except Exception as e: 130 | logger.error(f"Failed to send error message: {e}") 131 | 132 | async def handle_retry_after_error(update: Optional[Update], context: ContextTypes.DEFAULT_TYPE) -> None: 133 | """Handle RetryAfter errors.""" 134 | # Extract retry after time 135 | retry_after = context.error.retry_after if hasattr(context.error, 'retry_after') else 30 136 | 137 | if update and update.effective_message: 138 | error_message = f"Too many requests. Please try again after {retry_after} seconds." 139 | 140 | try: 141 | await update.effective_message.reply_text(error_message) 142 | except Exception as e: 143 | logger.error(f"Failed to send error message: {e}") 144 | 145 | async def handle_conflict_error(update: Optional[Update], context: ContextTypes.DEFAULT_TYPE) -> None: 146 | """Handle Conflict errors.""" 147 | if update and update.effective_message: 148 | error_message = "A conflict occurred. Please try again later." 149 | 150 | try: 151 | await update.effective_message.reply_text(error_message) 152 | except Exception as e: 153 | logger.error(f"Failed to send error message: {e}") 154 | 155 | async def handle_chat_migrated_error(update: Optional[Update], context: ContextTypes.DEFAULT_TYPE) -> None: 156 | """Handle ChatMigrated errors.""" 157 | # This error occurs when a group is migrated to a supergroup 158 | # We can extract the new chat id from the error 159 | new_chat_id = context.error.new_chat_id if hasattr(context.error, 'new_chat_id') else None 160 | 161 | logger.info(f"Chat migrated to {new_chat_id}") 162 | 163 | # No user-facing message needed for this error 164 | 165 | async def handle_invalid_token_error(update: Optional[Update], context: ContextTypes.DEFAULT_TYPE) -> None: 166 | """Handle InvalidToken errors.""" 167 | logger.critical("Invalid bot token provided!") 168 | 169 | # This is a critical error, no user-facing message can be sent 170 | 171 | async def handle_telegram_error(update: Optional[Update], context: ContextTypes.DEFAULT_TYPE) -> None: 172 | """Handle generic TelegramError errors.""" 173 | if update and update.effective_message: 174 | error_message = "An error occurred while processing your request. Please try again later." 175 | 176 | try: 177 | await update.effective_message.reply_text(error_message) 178 | except Exception as e: 179 | logger.error(f"Failed to send error message: {e}") 180 | -------------------------------------------------------------------------------- /src/handlers/payment_handlers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime, timedelta 3 | from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup 4 | from telegram.ext import ContextTypes 5 | from telegram.constants import ParseMode 6 | 7 | from services.subscription_service import verify_crypto_payment, get_plan_payment_details 8 | from database import update_user_premium_status, get_user 9 | 10 | async def handle_payment_verification(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 11 | """Handle payment verification process""" 12 | query = update.callback_query 13 | await query.answer("Verifying payment...") 14 | 15 | # Get transaction ID from context 16 | transaction_id = context.user_data.get("transaction_id") 17 | if not transaction_id: 18 | await query.edit_message_text( 19 | "❌ Error\n\n" 20 | "No transaction ID found. Please provide a transaction ID first.", 21 | parse_mode=ParseMode.HTML 22 | ) 23 | return 24 | 25 | # Get plan and currency 26 | callback_data = query.data 27 | parts = callback_data.split("_") 28 | if len(parts) < 4: 29 | await query.edit_message_text( 30 | "❌ Error\n\n" 31 | "Invalid callback data. Please try again.", 32 | parse_mode=ParseMode.HTML 33 | ) 34 | return 35 | 36 | plan = parts[2] 37 | currency = parts[3] 38 | 39 | # Get payment details 40 | from services.subscription_service import get_plan_payment_details 41 | try: 42 | payment_details = get_plan_payment_details(plan, currency) 43 | expected_amount = payment_details["amount"] 44 | wallet_address = payment_details["wallet_address"] 45 | duration_days = payment_details["duration_days"] 46 | network = payment_details["network"] 47 | except Exception as e: 48 | logging.error(f"Error getting payment details: {e}") 49 | await query.edit_message_text( 50 | f"❌ Error\n\n" 51 | f"Could not get payment details: {e}", 52 | parse_mode=ParseMode.HTML 53 | ) 54 | return 55 | 56 | # Verify payment 57 | verification_result = await verify_crypto_payment( 58 | transaction_id=transaction_id, 59 | expected_amount=expected_amount, 60 | wallet_address=wallet_address, 61 | network=network 62 | ) 63 | 64 | if verification_result["verified"]: 65 | # Payment verified, update user's premium status 66 | user_id = update.effective_user.id 67 | 68 | # Calculate premium expiration date 69 | now = datetime.now() 70 | premium_until = now + timedelta(days=duration_days) 71 | 72 | # Update user's premium status in the database 73 | update_user_premium_status( 74 | user_id=user_id, 75 | is_premium=True, 76 | premium_until=premium_until, 77 | plan=plan, 78 | payment_currency=currency, 79 | transaction_id=transaction_id 80 | ) 81 | 82 | # Clear transaction data from user_data 83 | if "transaction_id" in context.user_data: 84 | del context.user_data["transaction_id"] 85 | 86 | # Log successful premium activation 87 | logging.info(f"Premium activated for user {user_id}, plan: {plan}, currency: {currency}, until: {premium_until}") 88 | 89 | # Send confirmation to user 90 | await query.edit_message_text( 91 | f"✅ Payment Verified - Premium Activated!\n\n" 92 | f"Thank you for upgrading to Crypto DeFi Analyze Premium.\n\n" 93 | f"Transaction Details:\n" 94 | f"• Plan: {plan.capitalize()}\n" 95 | f"• Amount: {expected_amount} {currency.upper()}\n" 96 | f"• Transaction: {transaction_id[:8]}...{transaction_id[-6:]}\n\n" 97 | f"Your premium subscription is now active until: " 98 | f"{premium_until.strftime('%d %B %Y')}\n\n" 99 | f"Enjoy all the premium features!", 100 | reply_markup=reply_markup, 101 | parse_mode=ParseMode.HTML 102 | ) 103 | else: 104 | # Payment verification failed 105 | error_message = verification_result.get("error", "Unknown error") 106 | 107 | # Create helpful error message based on the specific error 108 | if "not found" in error_message.lower(): 109 | error_details = ( 110 | "• Transaction not found on the blockchain\n" 111 | "• The transaction may still be pending\n" 112 | "• Double-check that you entered the correct transaction ID" 113 | ) 114 | elif "wrong recipient" in error_message.lower(): 115 | error_details = ( 116 | "• Payment was sent to the wrong wallet address\n" 117 | "• Please ensure you sent to the correct address: " 118 | f"`{wallet_address[:10]}...{wallet_address[-8:]}`" 119 | ) 120 | elif "amount mismatch" in error_message.lower(): 121 | received = verification_result.get("received", 0) 122 | error_details = ( 123 | f"• Expected payment: {expected_amount} {currency.upper()}\n" 124 | f"• Received payment: {received} {currency.upper()}\n" 125 | "• Please ensure you sent the exact amount" 126 | ) 127 | elif "pending confirmation" in error_message.lower(): 128 | error_details = ( 129 | "• Transaction is still pending confirmation\n" 130 | "• Please wait for the transaction to be confirmed\n" 131 | "• Try again in a few minutes" 132 | ) 133 | else: 134 | error_details = ( 135 | "• Payment verification failed\n" 136 | "• The transaction may be invalid or incomplete\n" 137 | "• Please try again or contact support" 138 | ) 139 | 140 | # Create keyboard with options 141 | keyboard = [ 142 | [InlineKeyboardButton("Try Again", callback_data=f"payment_retry_{plan}_{currency}")], 143 | [InlineKeyboardButton("Contact Support", url="https://t.me/SeniorCrypto01")], 144 | [InlineKeyboardButton("🔙 Back", callback_data="premium_info")] 145 | ] 146 | reply_markup = InlineKeyboardMarkup(keyboard) 147 | 148 | # Send error message to user 149 | await query.edit_message_text( 150 | f"❌ Payment Verification Failed\n\n" 151 | f"We couldn't verify your payment:\n\n" 152 | f"{error_details}\n\n" 153 | f"Transaction ID: `{transaction_id[:10]}...{transaction_id[-8:]}`\n\n" 154 | f"Please try again or contact support for assistance.", 155 | reply_markup=reply_markup, 156 | parse_mode=ParseMode.HTML 157 | ) 158 | 159 | async def handle_payment_retry(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 160 | """Handle payment retry callback""" 161 | query = update.callback_query 162 | 163 | # Clear the stored transaction ID 164 | if "transaction_id" in context.user_data: 165 | del context.user_data["transaction_id"] 166 | 167 | # Get plan and currency from callback data 168 | callback_data = query.data 169 | parts = callback_data.split("_") 170 | if len(parts) < 4: 171 | await query.answer("Invalid callback data", show_alert=True) 172 | return 173 | 174 | plan = parts[2] 175 | currency = parts[3] 176 | 177 | # Set up to collect a new transaction ID 178 | context.user_data["awaiting_transaction_id"] = True 179 | context.user_data["premium_plan"] = plan 180 | context.user_data["payment_currency"] = currency 181 | 182 | keyboard = [[InlineKeyboardButton("🔙 Cancel", callback_data="premium_info")]] 183 | reply_markup = InlineKeyboardMarkup(keyboard) 184 | 185 | await query.edit_message_text( 186 | "📝 New Transaction ID Required\n\n" 187 | f"Please send the new transaction hash/ID of your {currency.upper()} payment.\n\n" 188 | "You can find this in your wallet's transaction history after sending the payment.", 189 | reply_markup=reply_markup, 190 | parse_mode=ParseMode.HTML 191 | ) 192 | -------------------------------------------------------------------------------- /src/services/user_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | User service for managing user data and premium status. 3 | """ 4 | 5 | import logging 6 | from typing import Dict, List, Optional, Any, Tuple, Literal 7 | from datetime import datetime, timedelta 8 | 9 | from models import User 10 | from database import * 11 | from services.wallet_service import create_wallet 12 | 13 | async def get_or_create_user(user_id: int, user_type: Literal["private", "group", "supergroup", "channel"] = "private", username: Optional[str] = None) -> User: 14 | """ 15 | Get a user from the database or create if not exists 16 | 17 | Args: 18 | user_id: The user's ID 19 | user_type: The type of Telegram entity (private, group, supergroup, channel) 20 | username: The user's username 21 | 22 | Returns: 23 | User object 24 | """ 25 | user = get_user(user_id) 26 | 27 | if not user: 28 | # Create new user 29 | user = User( 30 | user_id=user_id, 31 | user_type=user_type, 32 | username=username, 33 | ) 34 | 35 | # Create a wallet for the new user 36 | wallet_result = create_wallet("ETH") 37 | 38 | if wallet_result["success"]: 39 | wallet_info = wallet_result["data"] 40 | user.wallet_address = wallet_info["address"] 41 | user.wallet_private_key = wallet_info["private_key"] 42 | logging.info(f"Created new wallet for user {user_id}: {user.wallet_address}") 43 | else: 44 | logging.error(f"Failed to create wallet for user {user_id}: {wallet_result['error']}") 45 | 46 | save_user(user) 47 | else: 48 | # Update user activity 49 | update_user_activity(user_id) 50 | 51 | return user 52 | 53 | async def extend_premium_subscription(user_id: int, additional_days: int) -> bool: 54 | """ 55 | Extend an existing premium subscription 56 | 57 | Args: 58 | user_id: The user's ID 59 | additional_days: Number of days to extend the subscription by 60 | 61 | Returns: 62 | True if successful, False otherwise 63 | """ 64 | user = get_user(user_id) 65 | if not user: 66 | return False 67 | 68 | try: 69 | # Calculate new expiration date 70 | if user.is_premium and user.premium_until: 71 | # If already premium, add days to current expiration 72 | current_expiry = user.premium_until 73 | new_expiry = current_expiry + timedelta(days=additional_days) 74 | days_until_expiry = (new_expiry - datetime.now()).days 75 | set_premium_status(user_id, True, days_until_expiry) 76 | else: 77 | # If not premium, start new subscription 78 | set_premium_status(user_id, True, additional_days) 79 | 80 | return True 81 | except Exception as e: 82 | logging.error(f"Error extending premium subscription for user {user_id}: {e}") 83 | return False 84 | 85 | async def check_rate_limit(user_id: int, scan_type: str, limit: int) -> Tuple[bool, int]: 86 | """ 87 | Check if user has exceeded their daily scan limit 88 | 89 | Args: 90 | user_id: The user's ID 91 | scan_type: The type of scan (token_scan, wallet_scan, etc.) 92 | limit: The maximum number of scans allowed per day 93 | 94 | Returns: 95 | Tuple of (has_reached_limit, current_count) 96 | """ 97 | user = get_user(user_id) 98 | 99 | # Premium users have no limits 100 | if user and user.is_premium: 101 | return False, 0 102 | 103 | # Check scan count for today 104 | today = datetime.now().date().isoformat() 105 | scan_count = get_user_scan_count(user_id, scan_type, today) 106 | 107 | return scan_count >= limit, scan_count 108 | 109 | async def increment_scan_count(user_id: int, scan_type: str) -> int: 110 | """ 111 | Increment a user's scan count and return the new count 112 | 113 | Args: 114 | user_id: The user's ID 115 | scan_type: The type of scan (token_scan, wallet_scan, etc.) 116 | 117 | Returns: 118 | The new scan count 119 | """ 120 | today = datetime.now().date().isoformat() 121 | increment_user_scan_count(user_id, scan_type, today) 122 | return get_user_scan_count(user_id, scan_type, today) 123 | 124 | async def get_user_premium_info(user_id: int) -> Dict[str, Any]: 125 | """ 126 | Get information about a user's premium status 127 | 128 | Args: 129 | user_id: The user's ID 130 | 131 | Returns: 132 | Dictionary with premium status information 133 | """ 134 | user = get_user(user_id) 135 | if not user: 136 | return { 137 | "is_premium": False, 138 | "days_left": 0, 139 | "expiry_date": None 140 | } 141 | 142 | if not user.is_premium: 143 | return { 144 | "is_premium": False, 145 | "days_left": 0, 146 | "expiry_date": None 147 | } 148 | 149 | days_left = 0 150 | expiry_date = None 151 | 152 | if user.premium_until: 153 | days_left = max(0, (user.premium_until - datetime.now()).days) 154 | expiry_date = user.premium_until.strftime("%d %B %Y") 155 | 156 | return { 157 | "is_premium": user.is_premium, 158 | "days_left": days_left, 159 | "expiry_date": expiry_date 160 | } 161 | 162 | async def get_user_usage_stats(user_id: int) -> Dict[str, Any]: 163 | """ 164 | Get a user's usage statistics 165 | 166 | Args: 167 | user_id: The user's ID 168 | 169 | Returns: 170 | Dictionary with usage statistics 171 | """ 172 | user = get_user(user_id) 173 | if not user: 174 | return {} 175 | 176 | today = datetime.now().date().isoformat() 177 | yesterday = (datetime.now() - timedelta(days=1)).date().isoformat() 178 | 179 | # Get today's scan counts 180 | token_scans_today = get_user_scan_count(user_id, "token_scan", today) 181 | wallet_scans_today = get_user_scan_count(user_id, "wallet_scan", today) 182 | 183 | # Get yesterday's scan counts 184 | token_scans_yesterday = get_user_scan_count(user_id, "token_scan", yesterday) 185 | wallet_scans_yesterday = get_user_scan_count(user_id, "wallet_scan", yesterday) 186 | 187 | # Get tracking subscriptions from the watchlist tables 188 | from database import get_user_wallet_watchlist, get_user_token_watchlist, get_user_approval_watchlist 189 | wallet_watchlist = get_user_wallet_watchlist(user_id) 190 | token_watchlist = get_user_token_watchlist(user_id) 191 | approval_watchlist = get_user_approval_watchlist(user_id) 192 | 193 | # Count the number of entries in each watchlist 194 | wallet_tracks = len(wallet_watchlist) 195 | token_tracks = len(token_watchlist) 196 | deployer_tracks = len(approval_watchlist) 197 | 198 | return { 199 | "token_scans_today": token_scans_today, 200 | "wallet_scans_today": wallet_scans_today, 201 | "token_scans_yesterday": token_scans_yesterday, 202 | "wallet_scans_yesterday": wallet_scans_yesterday, 203 | "token_tracks": token_tracks, 204 | "wallet_tracks": wallet_tracks, 205 | "deployer_tracks": deployer_tracks, 206 | "is_premium": user.is_premium, 207 | "account_created": user.created_at.strftime("%d %B %Y") if user.created_at else "Unknown", 208 | "last_active": user.last_active.strftime("%d %B %Y %H:%M") if user.last_active else "Unknown" 209 | } 210 | 211 | async def cleanup_expired_premium_subscriptions() -> int: 212 | """ 213 | Clean up expired premium subscriptions 214 | 215 | Returns: 216 | The number of subscriptions that were expired 217 | """ 218 | try: 219 | # This function is in data.database 220 | from database import cleanup_expired_premium 221 | cleanup_expired_premium() 222 | 223 | # Count how many were expired (would need to be implemented in database.py) 224 | # For now, just return a placeholder 225 | return 0 226 | except Exception as e: 227 | logging.error(f"Error cleaning up expired premium subscriptions: {e}") 228 | return 0 229 | 230 | async def set_user_admin_status(user_id: int, is_admin: bool) -> bool: 231 | """ 232 | Set a user's admin status 233 | 234 | Args: 235 | user_id: The user's ID 236 | is_admin: Whether the user should be an admin 237 | 238 | Returns: 239 | True if successful, False otherwise 240 | """ 241 | try: 242 | from database import set_user_admin_status as db_set_user_admin_status 243 | db_set_user_admin_status(user_id, is_admin) 244 | return True 245 | except Exception as e: 246 | logging.error(f"Error setting admin status for user {user_id}: {e}") 247 | return False 248 | 249 | async def get_user_count_stats() -> Dict[str, int]: 250 | """ 251 | Get user count statistics 252 | 253 | Returns: 254 | Dictionary with user count statistics 255 | """ 256 | try: 257 | return get_user_counts() 258 | except Exception as e: 259 | logging.error(f"Error getting user count stats: {e}") 260 | return { 261 | "total_users": 0, 262 | "premium_users": 0, 263 | "active_today": 0, 264 | "active_week": 0, 265 | "active_month": 0 266 | } 267 | -------------------------------------------------------------------------------- /src/services/subscription_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | Subscription service for managing premium subscriptions. 3 | """ 4 | 5 | import logging 6 | from typing import Dict, Any, Optional, Literal 7 | from datetime import datetime, timedelta 8 | 9 | from config.settings import NETWORKS, SUBSCRIPTION_PLANS, ADMIN_WALLET_ADDRESS 10 | from services.wallet_service import create_wallet, check_wallet_balance 11 | 12 | def get_plan_payment_details( 13 | plan: str, 14 | currency: str, 15 | user_type: Literal["private", "group", "supergroup", "channel"] = "private" 16 | ) -> Dict[str, Any]: 17 | """ 18 | Get payment details for a subscription plan 19 | 20 | Args: 21 | plan: The subscription plan (weekly or monthly) 22 | currency: The payment currency (eth or bnb) 23 | user_type: The type of user (private, group, supergroup, channel) 24 | 25 | Returns: 26 | Dictionary with payment details 27 | """ 28 | if plan not in ["weekly", "monthly"]: 29 | raise ValueError(f"Invalid plan: {plan}") 30 | 31 | currency = currency.lower() 32 | if currency not in ["eth", "bnb"]: 33 | raise ValueError(f"Invalid currency: {currency}") 34 | 35 | # Determine if this is an individual or group/channel 36 | user_category = "individual" if user_type == "private" else "group_channel" 37 | 38 | # Get plan details 39 | plan_details = SUBSCRIPTION_PLANS[user_category][plan] 40 | 41 | price = plan_details[currency] 42 | network = currency.upper() 43 | 44 | # Create display name for the plan 45 | user_display = "individual" if user_type == "private" else "Group/Channel" 46 | display_name = f"{user_display} {plan.capitalize()} Plan" 47 | display_price = f"{price} {currency.upper()}" 48 | 49 | return { 50 | "plan": plan, 51 | "currency": currency.upper(), 52 | "amount": price, 53 | "duration_days": plan_details["duration_days"], 54 | "network": network, 55 | "display_name": display_name, 56 | "display_price": display_price, 57 | "user_type": user_type 58 | } 59 | 60 | def create_subscription( 61 | user_id: int, 62 | plan: str, 63 | network: str, 64 | user_type: Literal["private", "group", "supergroup", "channel"] = "private" 65 | ) -> Dict[str, Any]: 66 | """ 67 | Create a new subscription for a user, group, or channel. 68 | 69 | Args: 70 | user_id: The user ID 71 | plan: The subscription plan (weekly or monthly) 72 | network: The blockchain network (ETH, BNB, BASE) 73 | user_type: The type of user (private, group, supergroup, channel) 74 | 75 | Returns: 76 | Dictionary with subscription information or error details 77 | """ 78 | if plan not in ["weekly", "monthly"]: 79 | return { 80 | "success": False, 81 | "error": { 82 | "code": "INVALID_PLAN", 83 | "message": f"Invalid subscription plan: {plan}" 84 | } 85 | } 86 | 87 | if network not in NETWORKS: 88 | return { 89 | "success": False, 90 | "error": { 91 | "code": "INVALID_NETWORK", 92 | "message": f"Invalid network: {network}" 93 | } 94 | } 95 | 96 | from database import get_user, save_user 97 | 98 | # Get the user/group/channel from the database 99 | user = get_user(user_id) 100 | 101 | if not user: 102 | return { 103 | "success": False, 104 | "error": { 105 | "code": "USER_NOT_FOUND", 106 | "message": "User, group, or channel not found" 107 | } 108 | } 109 | 110 | if not user.wallet_address or not user.wallet_private_key: 111 | # If user doesn't have a wallet yet, create one 112 | wallet_result = create_wallet(network) 113 | if not wallet_result["success"]: 114 | return wallet_result 115 | 116 | wallet_info = wallet_result["data"] 117 | wallet_address = wallet_info["address"] 118 | wallet_private_key = wallet_info["private_key"] 119 | 120 | # Update user with new wallet 121 | user.wallet_address = wallet_address 122 | user.wallet_private_key = wallet_private_key 123 | save_user(user) 124 | else: 125 | # Use existing wallet 126 | wallet_address = user.wallet_address 127 | wallet_private_key = user.wallet_private_key 128 | 129 | # Calculate subscription expiry 130 | now = datetime.now() 131 | 132 | # Get payment details 133 | payment_details = get_plan_payment_details(plan, network.lower(), user_type) 134 | duration_days = payment_details["duration_days"] 135 | expiry = now + timedelta(days=duration_days) 136 | 137 | subscription = { 138 | "user_id": user_id, 139 | "user_type": user_type, 140 | "plan": plan, 141 | "network": network, 142 | "wallet_address": wallet_address, 143 | "amount": payment_details["amount"], 144 | "currency": payment_details["currency"], 145 | "created_at": now.isoformat(), 146 | "expires_at": expiry.isoformat(), 147 | "status": "pending_payment", 148 | "display_name": payment_details["display_name"], 149 | "display_price": payment_details["display_price"] 150 | } 151 | 152 | return { 153 | "success": True, 154 | "data": subscription 155 | } 156 | 157 | def check_subscription_payment( 158 | user_id: int, 159 | ) -> Dict[str, Any]: 160 | """ 161 | Check if payment has been received for a subscription 162 | 163 | Args: 164 | user_id: The user ID 165 | 166 | Returns: 167 | Dictionary with payment status or error details 168 | """ 169 | from database import get_user 170 | 171 | user = get_user(user_id) 172 | if not user: 173 | return { 174 | "success": False, 175 | "error": { 176 | "code": "USER_NOT_FOUND", 177 | "message": "User, group, or channel not found" 178 | } 179 | } 180 | 181 | balance_result = check_wallet_balance( 182 | user.wallet_address, 183 | user.payment_currency or "ETH" 184 | ) 185 | 186 | if not balance_result["success"]: 187 | return balance_result 188 | 189 | # Get required payment amount 190 | payment_details = get_plan_payment_details( 191 | user.premium_plan or "monthly", 192 | user.payment_currency or "eth", 193 | user.user_type 194 | ) 195 | 196 | # Check if balance is sufficient 197 | balance = balance_result["data"]["formatted_native_balance"] 198 | required_amount = payment_details["amount"] 199 | 200 | return { 201 | "success": True, 202 | "data": { 203 | "current_balance": balance, 204 | "required_amount": required_amount, 205 | "is_sufficient": balance >= required_amount, 206 | "payment_address": user.wallet_address 207 | } 208 | } 209 | 210 | def get_subscription_status(user_id: int) -> Dict[str, Any]: 211 | """ 212 | Get the status of a subscription 213 | 214 | Args: 215 | user_id: The user ID 216 | 217 | Returns: 218 | Dictionary with subscription status or error details 219 | """ 220 | from database import get_user 221 | 222 | user = get_user(user_id) 223 | if not user: 224 | return { 225 | "success": False, 226 | "error": { 227 | "code": "USER_NOT_FOUND", 228 | "message": "User, group, or channel not found" 229 | } 230 | } 231 | 232 | now = datetime.now() 233 | 234 | if not user.is_premium: 235 | return { 236 | "success": True, 237 | "data": { 238 | "is_premium": False, 239 | "plan": None, 240 | "expires_at": None, 241 | "days_remaining": 0, 242 | "user_type": user.user_type 243 | } 244 | } 245 | 246 | days_remaining = 0 247 | if user.premium_until: 248 | days_remaining = max(0, (user.premium_until - now).days) 249 | 250 | return { 251 | "success": True, 252 | "data": { 253 | "is_premium": user.is_premium, 254 | "plan": user.premium_plan, 255 | "expires_at": user.premium_until.isoformat() if user.premium_until else None, 256 | "days_remaining": days_remaining, 257 | "user_type": user.user_type 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/database/user_operations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Database operations related to users. 3 | """ 4 | 5 | import logging 6 | from typing import Optional, Dict, List, Any 7 | from datetime import datetime, timedelta 8 | 9 | from models import User, UserScan 10 | from services.subscription_service import get_plan_payment_details 11 | from .core import get_database 12 | 13 | def get_user(user_id: int) -> Optional[User]: 14 | """ 15 | Get a user by ID 16 | 17 | Args: 18 | user_id: The user's ID 19 | 20 | Returns: 21 | User object if found, None otherwise 22 | """ 23 | db = get_database() 24 | user_data = db.users.find_one({"user_id": user_id}) 25 | if user_data: 26 | return User.from_dict(user_data) 27 | return None 28 | 29 | def save_user(user: User) -> None: 30 | """ 31 | Save or update a user 32 | 33 | Args: 34 | user: The user object to save 35 | """ 36 | db = get_database() 37 | user_dict = user.to_dict() 38 | db.users.update_one( 39 | {"user_id": user.user_id}, 40 | {"$set": user_dict}, 41 | upsert=True 42 | ) 43 | 44 | def update_user_activity(user_id: int) -> None: 45 | """ 46 | Update user's last active timestamp 47 | 48 | Args: 49 | user_id: The user's ID 50 | """ 51 | db = get_database() 52 | db.users.update_one( 53 | {"user_id": user_id}, 54 | {"$set": {"last_active": datetime.now()}} 55 | ) 56 | 57 | def set_premium_status(user_id: int, is_premium: bool, duration_days: int = 30) -> None: 58 | """ 59 | Set a user's premium status 60 | 61 | Args: 62 | user_id: The user's ID 63 | is_premium: Whether the user should have premium status 64 | duration_days: Number of days the premium status should last 65 | """ 66 | db = get_database() 67 | premium_until = datetime.now() + timedelta(days=duration_days) if is_premium else None 68 | db.users.update_one( 69 | {"user_id": user_id}, 70 | {"$set": { 71 | "is_premium": is_premium, 72 | "premium_until": premium_until 73 | }} 74 | ) 75 | 76 | def get_user_scan_count(user_id: int, scan_type: str, date: str) -> int: 77 | """ 78 | Get the number of scans a user has performed of a specific type on a date 79 | 80 | Args: 81 | user_id: The user's ID 82 | scan_type: The type of scan 83 | date: The date in ISO format 84 | 85 | Returns: 86 | The number of scans performed 87 | """ 88 | db = get_database() 89 | scan_data = db.user_scans.find_one({ 90 | "user_id": user_id, 91 | "scan_type": scan_type, 92 | "date": date 93 | }) 94 | return scan_data.get("count", 0) if scan_data else 0 95 | 96 | def increment_user_scan_count(user_id: int, scan_type: str, date: str) -> None: 97 | """ 98 | Increment the scan count for a user 99 | 100 | Args: 101 | user_id: The user's ID 102 | scan_type: The type of scan 103 | date: The date in ISO format 104 | """ 105 | db = get_database() 106 | db.user_scans.update_one( 107 | { 108 | "user_id": user_id, 109 | "scan_type": scan_type, 110 | "date": date 111 | }, 112 | {"$inc": {"count": 1}}, 113 | upsert=True 114 | ) 115 | 116 | def reset_user_scan_counts() -> None: 117 | """Reset all user scan counts (typically called daily)""" 118 | db = get_database() 119 | today = datetime.now().date().isoformat() 120 | # Delete all scan records except today's 121 | db.user_scans.delete_many({"date": {"$ne": today}}) 122 | 123 | def update_user_premium_status( 124 | user_id: int, 125 | is_premium: bool, 126 | premium_until: datetime, 127 | plan: str, 128 | payment_currency: str = "eth", 129 | transaction_id: str = None, 130 | wallet_address: str = None, 131 | wallet_private_key: str = None 132 | ) -> None: 133 | """ 134 | Update a user's premium status in the database and record the transaction 135 | 136 | Args: 137 | user_id: The Telegram user ID 138 | is_premium: Whether the user has premium status 139 | premium_until: The date until which premium is active 140 | plan: The premium plan (weekly or monthly) 141 | payment_currency: The currency used for payment (eth or bnb) 142 | transaction_id: The payment transaction ID (optional) 143 | wallet_address: The wallet address used for payment (optional) 144 | wallet_private_key: The private key of the wallet (optional) 145 | """ 146 | try: 147 | # Get database connection 148 | db = get_database() 149 | 150 | # Update user premium status 151 | update_data = { 152 | "is_premium": is_premium, 153 | "premium_until": premium_until, 154 | "premium_plan": plan, 155 | "payment_currency": payment_currency, 156 | "last_payment_id": transaction_id, 157 | "updated_at": datetime.now() 158 | } 159 | 160 | # Add wallet information if provided 161 | if wallet_address: 162 | update_data["wallet_address"] = wallet_address 163 | 164 | if wallet_private_key: 165 | update_data["wallet_private_key"] = wallet_private_key 166 | 167 | db.users.update_one( 168 | {"user_id": user_id}, 169 | {"$set": update_data} 170 | ) 171 | 172 | # Get payment details 173 | payment_details = get_plan_payment_details(plan, payment_currency) 174 | 175 | # Record the transaction 176 | db.transactions.insert_one({ 177 | "user_id": user_id, 178 | "type": "premium_purchase", 179 | "plan_type": plan, 180 | "currency": payment_details["currency"], # Already uppercase from get_plan_payment_details 181 | "amount": payment_details["amount"], 182 | "duration_days": payment_details["duration_days"], 183 | "network": payment_details["network"], 184 | "transaction_id": transaction_id, 185 | "wallet_address": wallet_address, 186 | "date": datetime.now() 187 | }) 188 | 189 | logging.info(f"Updated premium status for user {user_id}: premium={is_premium}, plan={plan}, currency={payment_currency}, until={premium_until}") 190 | 191 | except Exception as e: 192 | logging.error(f"Error updating user premium status: {e}") 193 | raise 194 | 195 | def get_users_with_expiring_premium(days_left: List[int]) -> List[User]: 196 | """ 197 | Get users whose premium subscription is expiring in the specified number of days 198 | 199 | Args: 200 | days_left: List of days left before expiration to check for 201 | 202 | Returns: 203 | List of User objects with expiring premium 204 | """ 205 | db = get_database() 206 | now = datetime.now() 207 | 208 | # Calculate date ranges for the specified days left 209 | date_ranges = [] 210 | for days in days_left: 211 | start_date = now + timedelta(days=days) 212 | end_date = start_date + timedelta(days=1) 213 | date_ranges.append({"premium_until": {"$gte": start_date, "$lt": end_date}}) 214 | 215 | # Find users with premium expiring in any of the specified ranges 216 | users = db.users.find({ 217 | "is_premium": True, 218 | "$or": date_ranges 219 | }) 220 | 221 | return [User.from_dict(user) for user in users] 222 | 223 | def get_all_users() -> List[User]: 224 | """ 225 | Get all users in the database 226 | 227 | Returns: 228 | List of all User objects 229 | """ 230 | db = get_database() 231 | users = db.users.find() 232 | return [User.from_dict(user) for user in users] 233 | 234 | def get_admin_users() -> List[User]: 235 | """ 236 | Get all users with admin privileges 237 | 238 | Returns: 239 | List of User objects with admin privileges 240 | """ 241 | db = get_database() 242 | admin_users = db.users.find({"is_admin": True}) 243 | return [User.from_dict(user) for user in admin_users] 244 | 245 | def set_user_admin_status(user_id: int, is_admin: bool) -> None: 246 | """ 247 | Set a user's admin status 248 | 249 | Args: 250 | user_id: The user's ID 251 | is_admin: Whether the user should have admin status 252 | """ 253 | db = get_database() 254 | db.users.update_one( 255 | {"user_id": user_id}, 256 | {"$set": {"is_admin": is_admin}} 257 | ) 258 | 259 | def get_user_counts() -> Dict[str, int]: 260 | """ 261 | Get user count statistics 262 | 263 | Returns: 264 | Dictionary with user count statistics 265 | """ 266 | db = get_database() 267 | now = datetime.now() 268 | 269 | # Calculate date thresholds 270 | today_start = datetime.combine(now.date(), datetime.min.time()) 271 | week_ago = now - timedelta(days=7) 272 | month_ago = now - timedelta(days=30) 273 | 274 | # Get counts 275 | total_users = db.users.count_documents({}) 276 | premium_users = db.users.count_documents({"is_premium": True}) 277 | active_today = db.users.count_documents({"last_active": {"$gte": today_start}}) 278 | active_week = db.users.count_documents({"last_active": {"$gte": week_ago}}) 279 | active_month = db.users.count_documents({"last_active": {"$gte": month_ago}}) 280 | 281 | return { 282 | "total_users": total_users, 283 | "premium_users": premium_users, 284 | "active_today": active_today, 285 | "active_week": active_week, 286 | "active_month": active_month 287 | } 288 | 289 | def cleanup_expired_premium() -> None: 290 | """Remove premium status from users whose premium has expired""" 291 | db = get_database() 292 | now = datetime.now() 293 | db.users.update_many( 294 | { 295 | "is_premium": True, 296 | "premium_until": {"$lt": now} 297 | }, 298 | {"$set": { 299 | "is_premium": False, 300 | "premium_until": None 301 | }} 302 | ) 303 | -------------------------------------------------------------------------------- /src/database/watchlist_operations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Database operations related to watchlists. 3 | """ 4 | 5 | import logging 6 | from typing import List, Optional 7 | from datetime import datetime 8 | 9 | from models import WalletWatchlist, TokenWatchlist, ApprovalWatchlist 10 | from .core import get_database 11 | 12 | # Wallet Watchlist functions 13 | def get_user_wallet_watchlist(user_id: int) -> List[WalletWatchlist]: 14 | """ 15 | Get all wallet watchlist entries for a user 16 | 17 | Args: 18 | user_id: The user's ID 19 | 20 | Returns: 21 | List of WalletWatchlist objects 22 | """ 23 | db = get_database() 24 | watchlist = db.wallet_watchlist.find({ 25 | "user_id": user_id, 26 | "is_active": True 27 | }) 28 | return [WalletWatchlist.from_dict(item) for item in watchlist] 29 | 30 | def get_wallet_watchlist_entry(user_id: int, address: str, network: str) -> Optional[WalletWatchlist]: 31 | """ 32 | Get a specific wallet watchlist entry 33 | 34 | Args: 35 | user_id: The user's ID 36 | address: The wallet address 37 | network: The blockchain network 38 | 39 | Returns: 40 | WalletWatchlist object if found, None otherwise 41 | """ 42 | db = get_database() 43 | entry = db.wallet_watchlist.find_one({ 44 | "user_id": user_id, 45 | "address": address.lower(), 46 | "network": network 47 | }) 48 | if entry: 49 | return WalletWatchlist.from_dict(entry) 50 | return None 51 | 52 | def save_wallet_watchlist_entry(entry: WalletWatchlist) -> None: 53 | """ 54 | Save or update a wallet watchlist entry 55 | 56 | Args: 57 | entry: The watchlist entry to save 58 | """ 59 | db = get_database() 60 | entry_dict = entry.to_dict() 61 | entry_dict["address"] = entry_dict["address"].lower() # Normalize address 62 | 63 | db.wallet_watchlist.update_one( 64 | { 65 | "user_id": entry_dict["user_id"], 66 | "address": entry_dict["address"], 67 | "network": entry_dict["network"] 68 | }, 69 | {"$set": entry_dict}, 70 | upsert=True 71 | ) 72 | 73 | def delete_wallet_watchlist_entry(user_id: int, address: str, network: str) -> bool: 74 | """ 75 | Delete a wallet watchlist entry 76 | 77 | Args: 78 | user_id: The user's ID 79 | address: The wallet address 80 | network: The blockchain network 81 | 82 | Returns: 83 | True if the entry was deleted, False otherwise 84 | """ 85 | db = get_database() 86 | result = db.wallet_watchlist.delete_one({ 87 | "user_id": user_id, 88 | "address": address.lower(), 89 | "network": network 90 | }) 91 | return result.deleted_count > 0 92 | 93 | # Token Watchlist functions 94 | def get_user_token_watchlist(user_id: int) -> List[TokenWatchlist]: 95 | """ 96 | Get all token watchlist entries for a user 97 | 98 | Args: 99 | user_id: The user's ID 100 | 101 | Returns: 102 | List of TokenWatchlist objects 103 | """ 104 | db = get_database() 105 | watchlist = db.token_watchlist.find({ 106 | "user_id": user_id, 107 | "is_active": True 108 | }) 109 | return [TokenWatchlist.from_dict(item) for item in watchlist] 110 | 111 | def get_token_watchlist_entry(user_id: int, address: str, network: str) -> Optional[TokenWatchlist]: 112 | """ 113 | Get a specific token watchlist entry 114 | 115 | Args: 116 | user_id: The user's ID 117 | address: The token address 118 | network: The blockchain network 119 | 120 | Returns: 121 | TokenWatchlist object if found, None otherwise 122 | """ 123 | db = get_database() 124 | entry = db.token_watchlist.find_one({ 125 | "user_id": user_id, 126 | "address": address.lower(), 127 | "network": network 128 | }) 129 | if entry: 130 | return TokenWatchlist.from_dict(entry) 131 | return None 132 | 133 | def save_token_watchlist_entry(entry: TokenWatchlist) -> None: 134 | """ 135 | Save or update a token watchlist entry 136 | 137 | Args: 138 | entry: The watchlist entry to save 139 | """ 140 | db = get_database() 141 | entry_dict = entry.to_dict() 142 | entry_dict["address"] = entry_dict["address"].lower() # Normalize address 143 | 144 | db.token_watchlist.update_one( 145 | { 146 | "user_id": entry_dict["user_id"], 147 | "address": entry_dict["address"], 148 | "network": entry_dict["network"] 149 | }, 150 | {"$set": entry_dict}, 151 | upsert=True 152 | ) 153 | 154 | def delete_token_watchlist_entry(user_id: int, address: str, network: str) -> bool: 155 | """ 156 | Delete a token watchlist entry 157 | 158 | Args: 159 | user_id: The user's ID 160 | address: The token address 161 | network: The blockchain network 162 | 163 | Returns: 164 | True if the entry was deleted, False otherwise 165 | """ 166 | db = get_database() 167 | result = db.token_watchlist.delete_one({ 168 | "user_id": user_id, 169 | "address": address.lower(), 170 | "network": network 171 | }) 172 | return result.deleted_count > 0 173 | 174 | # Approval Watchlist functions 175 | def get_user_approval_watchlist(user_id: int) -> List[ApprovalWatchlist]: 176 | """ 177 | Get all approval watchlist entries for a user 178 | 179 | Args: 180 | user_id: The user's ID 181 | 182 | Returns: 183 | List of ApprovalWatchlist objects 184 | """ 185 | db = get_database() 186 | watchlist = db.approval_watchlist.find({ 187 | "user_id": user_id, 188 | "is_active": True 189 | }) 190 | return [ApprovalWatchlist.from_dict(item) for item in watchlist] 191 | 192 | def get_approval_watchlist_entry(user_id: int, address: str, network: str) -> Optional[ApprovalWatchlist]: 193 | """ 194 | Get a specific approval watchlist entry 195 | 196 | Args: 197 | user_id: The user's ID 198 | address: The wallet address 199 | network: The blockchain network 200 | 201 | Returns: 202 | ApprovalWatchlist object if found, None otherwise 203 | """ 204 | db = get_database() 205 | entry = db.approval_watchlist.find_one({ 206 | "user_id": user_id, 207 | "address": address.lower(), 208 | "network": network 209 | }) 210 | if entry: 211 | return ApprovalWatchlist.from_dict(entry) 212 | return None 213 | 214 | def save_approval_watchlist_entry(entry: ApprovalWatchlist) -> None: 215 | """ 216 | Save or update a approval watchlist entry 217 | 218 | Args: 219 | entry: The watchlist entry to save 220 | """ 221 | db = get_database() 222 | entry_dict = entry.to_dict() 223 | entry_dict["address"] = entry_dict["address"].lower() # Normalize address 224 | 225 | db.approval_watchlist.update_one( 226 | { 227 | "user_id": entry_dict["user_id"], 228 | "address": entry_dict["address"], 229 | "network": entry_dict["network"] 230 | }, 231 | {"$set": entry_dict}, 232 | upsert=True 233 | ) 234 | 235 | def delete_approval_watchlist_entry(user_id: int, address: str, network: str) -> bool: 236 | """ 237 | Delete a approval watchlist entry 238 | 239 | Args: 240 | user_id: The user's ID 241 | address: The wallet address 242 | network: The blockchain network 243 | 244 | Returns: 245 | True if the entry was deleted, False otherwise 246 | """ 247 | db = get_database() 248 | result = db.approval_watchlist.delete_one({ 249 | "user_id": user_id, 250 | "address": address.lower(), 251 | "network": network 252 | }) 253 | return result.deleted_count > 0 254 | 255 | def delete_tracking_subscription(user_id: int, tracking_type: str, target_address: str) -> None: 256 | """ 257 | Delete a tracking subscription 258 | 259 | Args: 260 | user_id: The user's ID 261 | tracking_type: The type of tracking (wallet, token, approval) 262 | target_address: The address being tracked 263 | """ 264 | db = get_database() 265 | db.tracking_subscriptions.delete_one({ 266 | "user_id": user_id, 267 | "tracking_type": tracking_type, 268 | "target_address": target_address.lower() 269 | }) 270 | 271 | def update_subscription_check_time(subscription_id: str) -> None: 272 | """ 273 | Update the last checked time for a subscription 274 | 275 | Args: 276 | subscription_id: The subscription ID 277 | """ 278 | db = get_database() 279 | db.tracking_subscriptions.update_one( 280 | {"_id": subscription_id}, 281 | {"$set": {"last_checked": datetime.now()}} 282 | ) 283 | 284 | # Functions to get all active watchlist entries (for background processing) 285 | def get_all_active_wallet_watchlist() -> List[WalletWatchlist]: 286 | """ 287 | Get all active wallet watchlist entries across all users 288 | 289 | Returns: 290 | List of all active WalletWatchlist objects 291 | """ 292 | db = get_database() 293 | entries = db.wallet_watchlist.find({"is_active": True}) 294 | return [WalletWatchlist.from_dict(entry) for entry in entries] 295 | 296 | def get_all_active_token_watchlist() -> List[TokenWatchlist]: 297 | """ 298 | Get all active token watchlist entries across all users 299 | 300 | Returns: 301 | List of all active TokenWatchlist objects 302 | """ 303 | db = get_database() 304 | entries = db.token_watchlist.find({"is_active": True}) 305 | return [TokenWatchlist.from_dict(entry) for entry in entries] 306 | 307 | def get_all_active_approval_watchlist() -> List[ApprovalWatchlist]: 308 | """ 309 | Get all active approval watchlist entries across all users 310 | 311 | Returns: 312 | List of all active ApprovalWatchlist objects 313 | """ 314 | db = get_database() 315 | entries = db.approval_watchlist.find({"is_active": True}) 316 | return [ApprovalWatchlist.from_dict(entry) for entry in entries] 317 | -------------------------------------------------------------------------------- /src/test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test script for blockchain_service.py functions 3 | Tests address validation for EVM chains and Solana 4 | """ 5 | 6 | import asyncio 7 | import logging 8 | from typing import List, Dict, Any, Tuple 9 | 10 | from web3 import Web3 11 | from solana.rpc.api import Client as SolanaClient 12 | 13 | # Import the functions to test 14 | from services.blockchain_service import ( 15 | is_valid_address, 16 | is_valid_solana_address, 17 | is_valid_token_contract, 18 | is_valid_wallet_address, 19 | get_web3_provider 20 | ) 21 | 22 | 23 | # Configure logging 24 | logging.basicConfig( 25 | level=logging.INFO, 26 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 27 | ) 28 | logger = logging.getLogger(__name__) 29 | 30 | # Test addresses 31 | TEST_ADDRESSES = { 32 | "eth": { 33 | "valid_wallet": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", 34 | "valid_contract": "0xdAC17F958D2ee523a2206206994597C13D831ec7", # USDT 35 | "invalid": "0x742d35Cc6634C0532925a3b844Bc454e4438f44", # Too short 36 | }, 37 | "bsc": { 38 | "valid_wallet": "0x8894E0a0c962CB723c1976a4421c95949bE2D4E3", 39 | "valid_contract": "0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56", # BUSD 40 | "invalid": "0x8894E0a0c962CB723c1976a4421c95949bE2D4E", # Too short 41 | }, 42 | "base": { 43 | "valid_wallet": "0x1a0ad011913A150f69f6A19DF447A0CfD9551054", 44 | "valid_contract": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC 45 | "invalid": "0x1a0ad011913A150f69f6A19DF447A0CfD955105", # Too short 46 | }, 47 | "sol": { 48 | "valid_wallet": "9oCqAtyb5ebgwMGU5Aotv1a3SDXwTRnzUzrhFkHXQUQM", 49 | "valid_contract": "3T721bpRc5FNY84W36vWffxoKs4FLXhBpSaqwUCRpump", # USDC on Solana 50 | "invalid": "NotASolanaAddress", 51 | } 52 | } 53 | 54 | async def test_is_valid_address(): 55 | """Test the is_valid_address function""" 56 | logger.info("Testing is_valid_address function...") 57 | 58 | results = [] 59 | 60 | # Test for each chain 61 | for chain, addresses in TEST_ADDRESSES.items(): 62 | # Test valid wallet address 63 | valid_wallet_result = await is_valid_address(addresses["valid_wallet"], chain) 64 | results.append({ 65 | "chain": chain, 66 | "address_type": "valid_wallet", 67 | "address": addresses["valid_wallet"], 68 | "expected": True, 69 | "actual": valid_wallet_result, 70 | "passed": valid_wallet_result == True 71 | }) 72 | 73 | # Test valid contract address 74 | valid_contract_result = await is_valid_address(addresses["valid_contract"], chain) 75 | results.append({ 76 | "chain": chain, 77 | "address_type": "valid_contract", 78 | "address": addresses["valid_contract"], 79 | "expected": True, 80 | "actual": valid_contract_result, 81 | "passed": valid_contract_result == True 82 | }) 83 | 84 | # Test invalid address 85 | invalid_result = await is_valid_address(addresses["invalid"], chain) 86 | results.append({ 87 | "chain": chain, 88 | "address_type": "invalid", 89 | "address": addresses["invalid"], 90 | "expected": False, 91 | "actual": invalid_result, 92 | "passed": invalid_result == False 93 | }) 94 | 95 | return results 96 | 97 | def test_is_valid_solana_address(): 98 | """Test the is_valid_solana_address function""" 99 | logger.info("Testing is_valid_solana_address function...") 100 | 101 | results = [] 102 | 103 | # Test valid Solana wallet address 104 | valid_wallet_result = is_valid_solana_address(TEST_ADDRESSES["sol"]["valid_wallet"]) 105 | results.append({ 106 | "address_type": "valid_wallet", 107 | "address": TEST_ADDRESSES["sol"]["valid_wallet"], 108 | "expected": True, 109 | "actual": valid_wallet_result, 110 | "passed": valid_wallet_result == True 111 | }) 112 | 113 | # Test valid Solana contract address 114 | valid_contract_result = is_valid_solana_address(TEST_ADDRESSES["sol"]["valid_contract"]) 115 | results.append({ 116 | "address_type": "valid_contract", 117 | "address": TEST_ADDRESSES["sol"]["valid_contract"], 118 | "expected": True, 119 | "actual": valid_contract_result, 120 | "passed": valid_contract_result == True 121 | }) 122 | 123 | # Test invalid Solana address 124 | invalid_result = is_valid_solana_address(TEST_ADDRESSES["sol"]["invalid"]) 125 | results.append({ 126 | "address_type": "invalid", 127 | "address": TEST_ADDRESSES["sol"]["invalid"], 128 | "expected": False, 129 | "actual": invalid_result, 130 | "passed": invalid_result == False 131 | }) 132 | 133 | return results 134 | 135 | async def test_is_valid_token_contract(): 136 | """Test the is_valid_token_contract function""" 137 | logger.info("Testing is_valid_token_contract function...") 138 | 139 | results = [] 140 | 141 | # Test for each chain 142 | for chain, addresses in TEST_ADDRESSES.items(): 143 | # Test valid contract address 144 | valid_contract_result = await is_valid_token_contract(addresses["valid_contract"], chain) 145 | results.append({ 146 | "chain": chain, 147 | "address_type": "valid_contract", 148 | "address": addresses["valid_contract"], 149 | "expected": True, 150 | "actual": valid_contract_result, 151 | "passed": valid_contract_result == True 152 | }) 153 | 154 | # Test valid wallet address (should return False as it's not a contract) 155 | valid_wallet_result = await is_valid_token_contract(addresses["valid_wallet"], chain) 156 | results.append({ 157 | "chain": chain, 158 | "address_type": "valid_wallet", 159 | "address": addresses["valid_wallet"], 160 | "expected": False, 161 | "actual": valid_wallet_result, 162 | "passed": valid_wallet_result == False 163 | }) 164 | 165 | # Test invalid address 166 | invalid_result = await is_valid_token_contract(addresses["invalid"], chain) 167 | results.append({ 168 | "chain": chain, 169 | "address_type": "invalid", 170 | "address": addresses["invalid"], 171 | "expected": False, 172 | "actual": invalid_result, 173 | "passed": invalid_result == False 174 | }) 175 | 176 | return results 177 | 178 | async def test_is_valid_wallet_address(): 179 | """Test the is_valid_wallet_address function""" 180 | logger.info("Testing is_valid_wallet_address function...") 181 | 182 | results = [] 183 | 184 | # Test for each chain 185 | for chain, addresses in TEST_ADDRESSES.items(): 186 | # Test valid wallet address 187 | valid_wallet_result = await is_valid_wallet_address(addresses["valid_wallet"], chain) 188 | results.append({ 189 | "chain": chain, 190 | "address_type": "valid_wallet", 191 | "address": addresses["valid_wallet"], 192 | "expected": True, 193 | "actual": valid_wallet_result, 194 | "passed": valid_wallet_result == True 195 | }) 196 | 197 | # Test valid contract address (should return False as it's not a wallet) 198 | valid_contract_result = await is_valid_wallet_address(addresses["valid_contract"], chain) 199 | results.append({ 200 | "chain": chain, 201 | "address_type": "valid_contract", 202 | "address": addresses["valid_contract"], 203 | "expected": False, 204 | "actual": valid_contract_result, 205 | "passed": valid_contract_result == False 206 | }) 207 | 208 | # Test invalid address 209 | invalid_result = await is_valid_wallet_address(addresses["invalid"], chain) 210 | results.append({ 211 | "chain": chain, 212 | "address_type": "invalid", 213 | "address": addresses["invalid"], 214 | "expected": False, 215 | "actual": invalid_result, 216 | "passed": invalid_result == False 217 | }) 218 | 219 | return results 220 | 221 | def test_get_web3_provider(): 222 | """Test the get_web3_provider function""" 223 | logger.info("Testing get_web3_provider function...") 224 | 225 | results = [] 226 | 227 | # Test for each chain 228 | for chain in TEST_ADDRESSES.keys(): 229 | provider = get_web3_provider(chain) 230 | expected_provider_name = f"w3_{chain}" if chain != "bsc" else "w3_bsc" 231 | 232 | # For Solana, the provider should be SolanaClient 233 | if chain == "sol": 234 | is_correct_type = isinstance(provider, SolanaClient) 235 | else: 236 | is_correct_type = isinstance(provider, Web3) 237 | 238 | results.append({ 239 | "chain": chain, 240 | "expected_type": "SolanaClient" if chain == "sol" else "Web3", 241 | "actual_type": type(provider).__name__, 242 | "passed": is_correct_type 243 | }) 244 | 245 | return results 246 | 247 | def print_test_results(test_name: str, results: List[Dict[str, Any]]): 248 | """Print the results of a test""" 249 | passed = sum(1 for result in results if result.get("passed", False)) 250 | total = len(results) 251 | 252 | logger.info(f"\n{'='*80}") 253 | logger.info(f"Test: {test_name}") 254 | logger.info(f"Passed: {passed}/{total} ({passed/total*100:.2f}%)") 255 | logger.info(f"{'-'*80}") 256 | 257 | for result in results: 258 | if not result.get("passed", False): 259 | if "chain" in result: 260 | logger.error(f"Failed: {result['chain']} - {result['address_type']} - {result['address']}") 261 | logger.error(f" Expected: {result['expected']}, Actual: {result['actual']}") 262 | else: 263 | logger.error(f"Failed: {result}") 264 | 265 | logger.info(f"{'='*80}\n") 266 | 267 | async def run_tests(): 268 | """Run all tests""" 269 | logger.info("Starting blockchain service tests...") 270 | 271 | # Test is_valid_address 272 | is_valid_address_results = await test_is_valid_address() 273 | print_test_results("is_valid_address", is_valid_address_results) 274 | 275 | # Test is_valid_solana_address 276 | is_valid_solana_address_results = test_is_valid_solana_address() 277 | print_test_results("is_valid_solana_address", is_valid_solana_address_results) 278 | 279 | # Test is_valid_token_contract 280 | is_valid_token_contract_results = await test_is_valid_token_contract() 281 | print_test_results("is_valid_token_contract", is_valid_token_contract_results) 282 | 283 | # Test is_valid_wallet_address 284 | is_valid_wallet_address_results = await test_is_valid_wallet_address() 285 | print_test_results("is_valid_wallet_address", is_valid_wallet_address_results) 286 | 287 | # Test get_web3_provider 288 | get_web3_provider_results = test_get_web3_provider() 289 | print_test_results("get_web3_provider", get_web3_provider_results) 290 | 291 | logger.info("All tests completed.") 292 | 293 | if __name__ == "__main__": 294 | # Run the tests 295 | asyncio.run(run_tests()) 296 | -------------------------------------------------------------------------------- /src/handlers/subscription_handlers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | from datetime import datetime, timedelta 4 | 5 | from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup 6 | from telegram.ext import ContextTypes, CallbackQueryHandler 7 | from telegram.constants import ParseMode 8 | 9 | from services.blockchain_service import check_wallet_balance 10 | from services.user_service import get_or_create_user 11 | from services.subscription_service import get_plan_payment_details 12 | 13 | # Add logging 14 | logger = logging.getLogger(__name__) 15 | 16 | async def handle_subscription_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 17 | """Handle subscription start button callback""" 18 | query = update.callback_query 19 | await query.answer() 20 | 21 | # Get the user/group/channel 22 | user_id = update.effective_chat.id 23 | user_type = update.effective_chat.type 24 | user = await get_or_create_user( 25 | user_id=user_id, 26 | user_type=user_type, 27 | username=update.effective_user.username if user_type == "private" else update.effective_chat.title, 28 | ) 29 | 30 | # Determine if this is an individual or group/channel 31 | is_group_or_channel = user_type in ["group", "supergroup", "channel"] 32 | 33 | # Create appropriate message based on entity type 34 | if is_group_or_channel: 35 | title = "Group/Channel" 36 | weekly_eth = "0.3 ETH" 37 | weekly_bnb = "1 BNB" 38 | monthly_eth = "1 ETH" 39 | monthly_bnb = "3 BNB" 40 | else: 41 | title = "Individual" 42 | weekly_eth = "0.1 ETH" 43 | weekly_bnb = "0.35 BNB" 44 | monthly_eth = "0.25 ETH" 45 | monthly_bnb = "1 BNB" 46 | 47 | keyboard = [ 48 | [ 49 | InlineKeyboardButton(f"🔄 Weekly ({weekly_eth})", callback_data="subscribe_weekly_eth"), 50 | InlineKeyboardButton(f"🔄 Weekly ({weekly_bnb})", callback_data="subscribe_weekly_bnb"), 51 | ], 52 | [ 53 | InlineKeyboardButton(f"📅 Monthly ({monthly_eth})", callback_data="subscribe_monthly_eth"), 54 | InlineKeyboardButton(f"📅 Monthly ({monthly_bnb})", callback_data="subscribe_monthly_bnb") 55 | ], 56 | [ 57 | InlineKeyboardButton(f"🔄 Refresh Balance", callback_data="refresh_balance"), 58 | ], 59 | ] 60 | 61 | reply_markup = InlineKeyboardMarkup(keyboard) 62 | 63 | balance = check_wallet_balance(user.wallet_address, "ETH") 64 | eth_amount = balance["data"]["formatted_native_balance"] 65 | balance = check_wallet_balance(user.wallet_address, "BSC") 66 | bnb_amount = balance["data"]["formatted_native_balance"] 67 | 68 | current_time = datetime.now().strftime("%H:%M:%S") 69 | 70 | await query.edit_message_text( 71 | f"💎 {title} Premium Subscription Options\n\n" 72 | f"Your Wallet Address:\n" 73 | f" 💼 {user.wallet_address}\n" 74 | f" 💵 ETH: {eth_amount}, BNB: {bnb_amount}\n" 75 | f" 🕒 Last updated: {current_time}\n\n" 76 | f"Choose your preferred subscription plan:\n" 77 | f" ✅ Weekly : {weekly_eth} or {weekly_bnb}\n" 78 | f" 👍🏻 Monthly : {monthly_eth} or {monthly_bnb}\n\n" 79 | f"✨ Premium benefits include:\n" 80 | f" 🔍 Unlimited token and wallet analysis\n" 81 | f" 🐳 Advanced whale tracking\n" 82 | f" 🚀 Priority access to new features\n" 83 | f" 📊 Unlimited wallet and token tracking\n", 84 | reply_markup=reply_markup, 85 | parse_mode=ParseMode.HTML 86 | ) 87 | 88 | async def handle_refresh_balance(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 89 | """Handle refresh balance button callback""" 90 | query = update.callback_query 91 | await query.answer("🔄 Refreshing wallet balance...") 92 | 93 | await handle_subscription_start(update, context) 94 | 95 | async def handle_subscription_selection(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 96 | """Handle subscription plan selection and payment verification in one step""" 97 | query = update.callback_query 98 | await query.answer() 99 | 100 | # Get the callback data which contains the subscription plan 101 | callback_data = query.data 102 | 103 | # Extract plan details from callback data (format: subscribe_period_chain) 104 | # Example: subscribe_weekly_eth 105 | parts = callback_data.split('_') 106 | if len(parts) != 3: 107 | await query.edit_message_text("Invalid subscription option. Please try again.") 108 | return 109 | 110 | period = parts[1] # weekly or monthly 111 | chain = parts[2] # eth or bnb 112 | 113 | # Get user information 114 | user_id = update.effective_chat.id 115 | user_type = update.effective_chat.type 116 | user = await get_or_create_user( 117 | user_id=user_id, 118 | user_type=user_type, 119 | username=update.effective_user.username if user_type == "private" else update.effective_chat.title, 120 | ) 121 | 122 | # Check if user has a wallet address and private key 123 | if not user.wallet_address: 124 | await query.edit_message_text( 125 | "❌ You need to set up a wallet address first. Please use the /wallet command to set your wallet address." 126 | ) 127 | return 128 | 129 | if not user.wallet_private_key: 130 | await query.edit_message_text( 131 | "❌ You need to set up your wallet private key to process payments. Please use the /setkey command." 132 | ) 133 | return 134 | 135 | # Get payment details for the selected plan 136 | payment_details = get_plan_payment_details(period, chain, user_type) 137 | 138 | if not payment_details: 139 | await query.edit_message_text("Invalid subscription plan. Please try again.") 140 | return 141 | 142 | # Send processing message 143 | await query.edit_message_text( 144 | f"🔍 Verifying payment for {period} subscription on {chain.upper()}... Please wait." 145 | ) 146 | 147 | # Check wallet balance 148 | balance_result = check_wallet_balance(user.wallet_address, chain.upper()) 149 | 150 | if not balance_result["success"]: 151 | await query.edit_message_text( 152 | f"❌ Error checking wallet balance: {balance_result['error']['message']}\n\n" 153 | f"Please try again later or contact support." 154 | ) 155 | return 156 | 157 | # Get the required amount for the subscription 158 | required_amount = payment_details["amount"] 159 | user_balance = float(balance_result["data"]["formatted_native_balance"]) 160 | 161 | # Check if user has enough balance 162 | if user_balance < required_amount: 163 | keyboard = [ 164 | [InlineKeyboardButton("🔄 Refresh Balance", callback_data="refresh_balance")], 165 | [InlineKeyboardButton("🔙 Back to Plans", callback_data="subscription_start")] 166 | ] 167 | reply_markup = InlineKeyboardMarkup(keyboard) 168 | 169 | await query.edit_message_text( 170 | f"❌ Insufficient Balance\n\n" 171 | f"Your {chain.upper()} balance: {user_balance:.4f}\n" 172 | f"Required for {period} subscription: {required_amount:.4f}\n\n" 173 | f"Please add funds to your wallet and try again.", 174 | reply_markup=reply_markup, 175 | parse_mode=ParseMode.HTML 176 | ) 177 | return 178 | 179 | # Process payment 180 | try: 181 | # Update processing message 182 | await query.edit_message_text( 183 | f"💸 Processing payment... Please wait." 184 | ) 185 | 186 | # Create subscription object for transfer 187 | subscription = { 188 | "network": chain.upper(), 189 | "require_amount": required_amount, # Pass the amount in ETH/BNB, not Wei 190 | "wallet_address": user.wallet_address, 191 | "wallet_private_key": user.wallet_private_key 192 | } 193 | 194 | # Transfer funds to admin wallet 195 | from services.blockchain_service import transfer_funds_to_admin 196 | transfer_result = transfer_funds_to_admin(subscription) 197 | 198 | if not transfer_result["success"]: 199 | await query.edit_message_text( 200 | f"❌ Payment Failed\n\n" 201 | f"Error: {transfer_result['error']['message']}\n" 202 | f"Please try again later or contact support.", 203 | parse_mode=ParseMode.HTML 204 | ) 205 | return 206 | 207 | # Calculate subscription end date 208 | if period == "weekly": 209 | end_date = datetime.now() + timedelta(days=7) 210 | else: # monthly 211 | end_date = datetime.now() + timedelta(days=30) 212 | 213 | # Update user subscription status using the database operations 214 | from database.user_operations import update_user_premium_status 215 | 216 | # Update user premium status in the database 217 | update_user_premium_status( 218 | user_id=user.user_id, 219 | is_premium=True, 220 | premium_until=end_date, 221 | plan=period, 222 | payment_currency=chain, 223 | transaction_id=transfer_result["data"]["transaction_hash"], 224 | wallet_address=user.wallet_address 225 | ) 226 | 227 | # Update the user object to reflect the changes 228 | user.is_premium = True 229 | user.premium_until = end_date 230 | user.premium_plan = period 231 | user.payment_currency = chain 232 | 233 | # Format the amount with proper decimal places 234 | amount_display = f"{float(transfer_result['data']['amount']):.4f}" 235 | 236 | await query.edit_message_text( 237 | f"✅ Subscription Activated Successfully!\n\n" 238 | f"Plan: {period.capitalize()} ({chain.upper()})\n" 239 | f"Amount: {amount_display} {chain.upper()}\n" 240 | f"Transaction: {transfer_result['data']['transaction_hash']}\n" 241 | f"Valid until: {end_date.strftime('%Y-%m-%d %H:%M')}\n\n" 242 | f"Thank you for subscribing to our premium service! You now have access to all premium features.\n\n" 243 | f"Premium Benefits:\n" 244 | f"• Unlimited token and wallet analysis\n" 245 | f"• Advanced whale tracking\n" 246 | f"• Priority access to new features\n" 247 | f"• Unlimited wallet and token tracking", 248 | parse_mode=ParseMode.HTML 249 | ) 250 | 251 | # Log the successful subscription 252 | logger.info(f"User {user.user_id} ({user.username}) subscribed to {period} plan with {chain}. Transaction: {transfer_result['data']['transaction_hash']}") 253 | 254 | except Exception as e: 255 | # Improved error logging with traceback 256 | import traceback 257 | error_traceback = traceback.format_exc() 258 | logging.error(f"Error processing subscription: {str(e)}\n{error_traceback}") 259 | 260 | # Get a more detailed error message 261 | if hasattr(e, 'details'): 262 | error_details = e.details 263 | elif hasattr(e, 'args') and e.args: 264 | error_details = str(e.args[0]) 265 | else: 266 | error_details = str(e) 267 | 268 | keyboard = [ 269 | [InlineKeyboardButton("🔙 Back to Plans", callback_data="subscription_start")] 270 | ] 271 | reply_markup = InlineKeyboardMarkup(keyboard) 272 | 273 | await query.edit_message_text( 274 | f"❌ Error Processing Subscription\n\n" 275 | f"An error occurred while processing your subscription. Please try again later or contact support.\n\n" 276 | f"Error details: {error_details}", 277 | reply_markup=reply_markup, 278 | parse_mode=ParseMode.HTML 279 | ) 280 | 281 | def register_subscription_handlers(application): 282 | """Register all subscription handlers""" 283 | application.add_handler(CallbackQueryHandler(handle_subscription_start, pattern="^subscription_start$")) 284 | application.add_handler(CallbackQueryHandler(handle_refresh_balance, pattern="^refresh_balance$")) 285 | application.add_handler(CallbackQueryHandler(handle_subscription_selection, pattern="^subscribe_")) -------------------------------------------------------------------------------- /src/services/blockchain_service.py: -------------------------------------------------------------------------------- 1 | """ 2 | Core blockchain interaction service. 3 | Provides low-level functions for interacting with various blockchains. 4 | """ 5 | 6 | import logging 7 | import base58 8 | import re 9 | import time 10 | import random 11 | from datetime import datetime 12 | from typing import Dict, Optional, Any, List 13 | from web3 import Web3 14 | from web3.middleware import ExtraDataToPOAMiddleware 15 | 16 | from solana.rpc.api import Client as SolanaClient 17 | from solders.pubkey import Pubkey 18 | from solders.keypair import Keypair 19 | 20 | from config.settings import ADMIN_WALLET_ADDRESS, ETH_PROVIDER_URL, BNB_PROVIDER_URL, BASE_PROVIDER_URL, SOL_PROVIDER_URL, SOLANA_TOKEN_PROGRAM_ID 21 | 22 | # Configure web3 connections for different networks 23 | w3_eth = Web3(Web3.HTTPProvider(ETH_PROVIDER_URL)) 24 | w3_base = Web3(Web3.HTTPProvider(BASE_PROVIDER_URL)) 25 | w3_bsc = Web3(Web3.HTTPProvider(BNB_PROVIDER_URL)) 26 | w3_sol = SolanaClient(SOL_PROVIDER_URL) 27 | 28 | # Add PoA middleware for networks like BSC 29 | w3_eth.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0) 30 | w3_bsc.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0) 31 | 32 | # ERC20 ABI for basic token operations 33 | ERC20_ABI = [ 34 | { 35 | "constant": True, 36 | "inputs": [{"name": "_owner", "type": "address"}], 37 | "name": "balanceOf", 38 | "outputs": [{"name": "balance", "type": "uint256"}], 39 | "type": "function" 40 | }, 41 | { 42 | "constant": False, 43 | "inputs": [ 44 | {"name": "_to", "type": "address"}, 45 | {"name": "_value", "type": "uint256"} 46 | ], 47 | "name": "transfer", 48 | "outputs": [{"name": "", "type": "bool"}], 49 | "type": "function" 50 | }, 51 | { 52 | "constant": True, 53 | "inputs": [], 54 | "name": "name", 55 | "outputs": [{"name": "", "type": "string"}], 56 | "payable": False, 57 | "stateMutability": "view", 58 | "type": "function" 59 | }, 60 | { 61 | "constant": True, 62 | "inputs": [], 63 | "name": "symbol", 64 | "outputs": [{"name": "", "type": "string"}], 65 | "payable": False, 66 | "stateMutability": "view", 67 | "type": "function" 68 | }, 69 | { 70 | "constant": True, 71 | "inputs": [], 72 | "name": "decimals", 73 | "outputs": [{"name": "", "type": "uint8"}], 74 | "payable": False, 75 | "stateMutability": "view", 76 | "type": "function" 77 | } 78 | ] 79 | 80 | def get_web3_provider(chain: str) -> Web3: 81 | """ 82 | Get the appropriate Web3 provider for the specified chain 83 | 84 | Args: 85 | chain: The blockchain network (eth, base, bsc) 86 | 87 | Returns: 88 | Web3: The Web3 provider for the specified chain 89 | """ 90 | if chain.lower() == "eth": 91 | return w3_eth 92 | elif chain.lower() == "base": 93 | return w3_base 94 | elif chain.lower() == "bsc" or chain.lower() == "bnb": 95 | return w3_bsc 96 | elif chain.lower() == "sol": 97 | return w3_sol 98 | else: 99 | logging.warning(f"Unknown chain '{chain}', defaulting to Ethereum") 100 | return w3_eth 101 | 102 | async def is_valid_address(address:str, chain:str) -> bool: 103 | """ 104 | Validates if the given string is a valid EVM address. 105 | 106 | Args: 107 | address: The address to validate 108 | 109 | Returns: 110 | bool: True if the address is valid, False otherwise 111 | """ 112 | if not address: 113 | return False 114 | 115 | # Check if address matches the format (0x followed by 40 hex characters) 116 | if re.match(r'^0x[0-9a-fA-F]{40}$', address): 117 | return True 118 | 119 | if chain and chain.lower() == 'sol': 120 | return is_valid_solana_address(address) 121 | 122 | return False 123 | 124 | def is_valid_solana_address(address): 125 | try: 126 | # Solana addresses are base58 encoded and 32 bytes (after decoding) 127 | decoded = base58.b58decode(address) 128 | return len(decoded) == 32 129 | except Exception: 130 | return False 131 | 132 | async def is_valid_token_contract(address: str, chain: str) -> bool: 133 | """ 134 | Check if an address is a valid token contract 135 | 136 | Args: 137 | address: The address to check 138 | chain: The blockchain network 139 | 140 | Returns: 141 | bool: True if the address is a valid token contract, False otherwise 142 | """ 143 | if not await is_valid_address(address, chain): 144 | logging.warning(f"Invalid address format for checking is_valid_token_contract: {address} on {chain}") 145 | return False 146 | 147 | w3 = get_web3_provider(chain) 148 | 149 | if chain.lower() == "sol": 150 | max_retries = 5 151 | retry_count = 0 152 | retry_delay = 1 # Start with 1 second delay 153 | 154 | while retry_count < max_retries: 155 | try: 156 | try: 157 | pubkey = Pubkey.from_string(address) 158 | except Exception as e: 159 | logging.warning(f"Invalid Solana pubkey format: {e}") 160 | return False 161 | 162 | account_info = w3.get_account_info(pubkey) 163 | 164 | if not account_info.value: 165 | logging.info(f"No account found for Solana address: {address}") 166 | return False 167 | 168 | if str(account_info.value.owner) != SOLANA_TOKEN_PROGRAM_ID: 169 | logging.info(f"Solana address is not owned by Token program: {address}") 170 | return False 171 | 172 | return True 173 | except Exception as e: 174 | retry_count += 1 175 | if retry_count >= max_retries: 176 | logging.error(f"Error validating Solana token after {max_retries} retries: {e}") 177 | return False 178 | 179 | # Log the retry attempt 180 | logging.warning(f"Rate limit hit when validating Solana token. Retrying {retry_count}/{max_retries} in {retry_delay}s: {e}") 181 | 182 | # Exponential backoff with jitter 183 | time.sleep(retry_delay + (random.random() * 0.5)) 184 | retry_delay = min(retry_delay * 2, 10) # Double the delay up to a maximum of 10 seconds 185 | else: 186 | try: 187 | checksum_address = w3.to_checksum_address(address.lower()) 188 | code = w3.eth.get_code(checksum_address) 189 | 190 | if code == b'' or code == b'0x': 191 | logging.info("Address has no contract code.") 192 | return False 193 | 194 | contract = w3.eth.contract(address=checksum_address, abi=ERC20_ABI) 195 | 196 | # Try to call basic ERC20 functions to verify it's a token 197 | try: 198 | symbol = contract.functions.symbol().call() 199 | logging.info(f"Token symbol: {symbol}") 200 | except Exception as e: 201 | logging.warning(f"Couldn't get token symbol: {e}") 202 | 203 | try: 204 | name = contract.functions.name().call() 205 | logging.info(f"Token name: {name}") 206 | return True 207 | except Exception as e: 208 | logging.warning(f"Couldn't get token name: {e}") 209 | 210 | try: 211 | decimals = contract.functions.decimals().call() 212 | logging.info(f"Token decimals: {decimals}") 213 | return True 214 | except Exception as e: 215 | logging.warning(f"Couldn't get token decimals: {e}") 216 | 217 | logging.warning("Address has code but no ERC-20 behavior.") 218 | return False 219 | 220 | except Exception as e: 221 | logging.error(f"Error validating token contract: {e}") 222 | return False 223 | 224 | async def is_valid_wallet_address(address: str, chain: str) -> bool: 225 | """ 226 | Validate if the provided address is a wallet (not a contract) 227 | 228 | Args: 229 | address: The address to validate 230 | chain: The blockchain network (eth, base, bsc) 231 | 232 | Returns: 233 | bool: True if the address is a valid wallet, False otherwise 234 | """ 235 | # First check if it's a valid address 236 | if not await is_valid_address(address, chain): 237 | return False 238 | 239 | if chain.lower() == "sol": 240 | try: 241 | is_token = await is_valid_token_contract(address, chain) 242 | return not is_token 243 | except Exception as e: 244 | logging.error(f"Error validating Solana wallet address: {e}") 245 | return True 246 | 247 | w3 = get_web3_provider(chain) 248 | 249 | try: 250 | checksum_address = w3.to_checksum_address(address.lower()) 251 | code = w3.eth.get_code(checksum_address) 252 | # If there's no code, it's a regular wallet address 253 | return code == b'' or code == '0x' 254 | except Exception as e: 255 | logging.error(f"Error validating wallet address on {chain}: {e}") 256 | # Return True if the format is correct but web3 validation fails 257 | # This is a fallback to prevent false negatives due to connection issues 258 | return True 259 | 260 | def check_wallet_balance(address: str, network: str, token_address: Optional[str] = None) -> Dict[str, Any]: 261 | """ 262 | Check the balance of a wallet on the specified network. 263 | 264 | Args: 265 | address: The wallet address to check 266 | network: The network (ETH, BSC, BASE) 267 | token_address: Optional ERC20/BEP20 token address to check balance for 268 | 269 | Returns: 270 | Dictionary with balance information or error details 271 | """ 272 | retry_count = 0 273 | max_retries = 3 274 | 275 | logging.info(f"Checking balance for {address} on {network}") 276 | 277 | while retry_count < max_retries: 278 | try: 279 | w3 = get_web3_provider(network) 280 | 281 | # Validate address format 282 | if not w3.is_address(address): 283 | logging.error(f"Invalid {network} address format: {address}") 284 | return { 285 | "success": False, 286 | "error": { 287 | "code": "INVALID_ADDRESS", 288 | "message": f"Invalid {network} address format" 289 | } 290 | } 291 | 292 | result = { 293 | "success": True, 294 | "data": { 295 | "address": address, 296 | "network": network, 297 | "timestamp": datetime.now().isoformat() 298 | } 299 | } 300 | 301 | # Check native currency balance (ETH/BNB) 302 | native_balance = w3.eth.get_balance(address) 303 | result["data"]["native_balance"] = native_balance 304 | result["data"]["formatted_native_balance"] = w3.from_wei(native_balance, 'ether') 305 | result["data"]["native_currency"] = network 306 | 307 | # Check if balance is sufficient for gas fees 308 | gas_price = w3.eth.gas_price 309 | standard_gas_limit = 21000 # For a basic transfer 310 | estimated_gas_cost = gas_price * standard_gas_limit 311 | result["data"]["estimated_gas_cost"] = estimated_gas_cost 312 | result["data"]["formatted_gas_cost"] = w3.from_wei(estimated_gas_cost, 'ether') 313 | result["data"]["has_sufficient_gas"] = native_balance > estimated_gas_cost 314 | 315 | # Check token balance if token address is provided 316 | if token_address: 317 | if not w3.is_address(token_address): 318 | result["data"]["token_error"] = "Invalid token address format" 319 | else: 320 | try: 321 | token_contract = w3.eth.contract(address=token_address, abi=ERC20_ABI) 322 | token_balance = token_contract.functions.balanceOf(address).call() 323 | token_decimals = 18 # Default for most tokens 324 | 325 | # Try to get token decimals if the function exists 326 | try: 327 | token_decimals = token_contract.functions.decimals().call() 328 | except Exception: 329 | pass # Use default decimals if not available 330 | 331 | result["data"]["token_address"] = token_address 332 | result["data"]["token_balance"] = token_balance 333 | result["data"]["formatted_token_balance"] = token_balance / (10 ** token_decimals) 334 | except Exception as token_error: 335 | result["data"]["token_error"] = str(token_error) 336 | 337 | logging.info(f"Balance check for {address} on {network}: {result['data']['formatted_native_balance']} {network}") 338 | return result 339 | 340 | except Exception as e: 341 | retry_count += 1 342 | if retry_count >= max_retries: 343 | logging.error(f"Failed to check {network} balance for {address} after {max_retries} attempts: {str(e)}") 344 | return { 345 | "success": False, 346 | "error": { 347 | "code": "BALANCE_CHECK_FAILED", 348 | "message": f"Failed to check {network} wallet balance", 349 | "details": str(e) 350 | } 351 | } 352 | else: 353 | logging.warning(f"Retry {retry_count}/{max_retries} checking balance for {address} on {network}") 354 | time.sleep(1) 355 | 356 | def transfer_funds_to_admin(subscription: Dict[str, Any]) -> Dict[str, Any]: 357 | """Transfer funds from user wallet to admin wallet.""" 358 | try: 359 | network = subscription["network"] 360 | web3 = get_web3_provider(network) 361 | 362 | user_address = subscription["wallet_address"] 363 | user_private_key = subscription["wallet_private_key"] 364 | admin_address = ADMIN_WALLET_ADDRESS 365 | 366 | if not web3.is_address(admin_address): 367 | return { 368 | "success": False, 369 | "error": { 370 | "code": "INVALID_ADMIN_ADDRESS", 371 | "message": f"Invalid admin address for {network}" 372 | } 373 | } 374 | 375 | # Get user balance 376 | balance = web3.eth.get_balance(user_address) 377 | 378 | # Calculate gas cost - BNB Chain often needs different gas settings 379 | if network == "BNB": 380 | gas_price = web3.eth.gas_price 381 | # BNB Chain sometimes needs a higher gas price 382 | gas_price = int(gas_price * 1.1) # Add 10% to ensure it goes through 383 | gas_limit = 21000 # Standard gas limit 384 | else: 385 | gas_price = web3.eth.gas_price 386 | gas_limit = 21000 # Standard gas limit for ETH transfer 387 | 388 | gas_cost = gas_price * gas_limit 389 | 390 | # Get the required amount in Wei 391 | required_amount = subscription["require_amount"] 392 | 393 | # Check if the required_amount is already in Wei or needs conversion 394 | if isinstance(required_amount, float) or isinstance(required_amount, int) and required_amount < 1e18: 395 | # Convert from ETH to Wei if it's a small number (likely in ETH) 396 | required_amount_wei = web3.to_wei(required_amount, 'ether') 397 | else: 398 | # Already in Wei 399 | required_amount_wei = required_amount 400 | 401 | # Check if user has enough balance for amount + gas 402 | amount_to_send = required_amount_wei - gas_cost 403 | 404 | if balance < amount_to_send: 405 | return { 406 | "success": False, 407 | "error": { 408 | "code": "INSUFFICIENT_BALANCE", 409 | "message": f"Insufficient balance for transaction. Need {web3.from_wei(amount_to_send, 'ether'):.6f} {network} (including gas), but have {web3.from_wei(balance, 'ether'):.6f} {network}" 410 | } 411 | } 412 | 413 | # Get the actual chain ID from the connected network 414 | chain_id = web3.eth.chain_id 415 | 416 | # Build transaction 417 | tx = { 418 | 'nonce': web3.eth.get_transaction_count(user_address), 419 | 'to': admin_address, 420 | 'value': amount_to_send, 421 | 'gas': gas_limit, 422 | 'gasPrice': gas_price, 423 | 'chainId': chain_id 424 | } 425 | 426 | # Sign and send transaction 427 | signed_tx = web3.eth.account.sign_transaction(tx, user_private_key) 428 | 429 | # Handle different web3.py versions 430 | if hasattr(signed_tx, 'rawTransaction'): 431 | tx_hash = web3.eth.send_raw_transaction(signed_tx.rawTransaction) 432 | else: 433 | tx_hash = web3.eth.send_raw_transaction(signed_tx.raw_transaction) 434 | 435 | return { 436 | "success": True, 437 | "data": { 438 | "transaction_hash": tx_hash.hex(), 439 | "amount": web3.from_wei(amount_to_send, 'ether') 440 | } 441 | } 442 | except Exception as e: 443 | logging.error(f"Transfer funds error: {str(e)}") 444 | return { 445 | "success": False, 446 | "error": { 447 | "code": "TRANSFER_FAILED", 448 | "message": "Failed to transfer funds to admin wallet", 449 | "details": str(e) 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /src/database/token_operations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Database operations related to tokens. 3 | """ 4 | 5 | import logging 6 | from typing import Optional, Dict, List, Any, Tuple 7 | from datetime import datetime 8 | 9 | from models import TokenData 10 | from api.token_api import ( 11 | fetch_first_buyers, fetch_token_profitable_wallets, fetch_market_cap, 12 | fetch_token_deployer_projects, fetch_token_holders, fetch_token_metadata, 13 | fetch_token_details, 14 | ) 15 | from api.wallet_api import fetch_high_net_worth_holders 16 | from .core import get_database 17 | 18 | def get_tokendata(address: str) -> Optional[TokenData]: 19 | """ 20 | Get token data by address 21 | 22 | Args: 23 | address: The token contract address 24 | 25 | Returns: 26 | TokenData object if found, None otherwise 27 | """ 28 | db = get_database() 29 | token_data = db.token_data.find_one({"address": address.lower()}) 30 | if token_data: 31 | return TokenData.from_dict(token_data) 32 | return None 33 | 34 | def save_token_data(token: TokenData) -> None: 35 | """ 36 | Save or update token data 37 | 38 | Args: 39 | token: The token data object to save 40 | """ 41 | db = get_database() 42 | token_dict = token.to_dict() 43 | token_dict["address"] = token_dict["address"].lower() # Normalize address 44 | token_dict["last_updated"] = datetime.now() 45 | 46 | db.token_data.update_one( 47 | {"address": token_dict["address"]}, 48 | {"$set": token_dict}, 49 | upsert=True 50 | ) 51 | 52 | def get_tokens_by_deployer(deployer_address: str) -> List[TokenData]: 53 | """ 54 | Get all tokens deployed by a specific address 55 | 56 | Args: 57 | deployer_address: The deployer wallet address 58 | 59 | Returns: 60 | List of TokenData objects deployed by the address 61 | """ 62 | db = get_database() 63 | tokens = db.token_data.find({"deployer": deployer_address.lower()}) 64 | return [TokenData.from_dict(token) for token in tokens] 65 | 66 | async def get_token_metadata(token_address: str, chain: str) -> Dict[str, Any]: 67 | """ 68 | Get detailed token metadata for a specific token 69 | 70 | Args: 71 | token_address: The token contract address 72 | chain: The blockchain network (eth, base, bsc) 73 | 74 | Returns: 75 | Dictionary containing token metadata including name, symbol, decimals, 76 | total supply, logo, links, categories, etc. 77 | """ 78 | logging.info(f"Getting token metadata for {token_address} on {chain}") 79 | 80 | try: 81 | # Fetch token metadata from API 82 | response = await fetch_token_metadata(chain, token_address) 83 | 84 | if not response: 85 | logging.warning(f"No metadata found for token {token_address} on {chain}") 86 | return None 87 | 88 | # Extract relevant data from response 89 | metadata = { 90 | "address": response.get("address"), 91 | "name": response.get("name"), 92 | "symbol": response.get("symbol"), 93 | "decimals": response.get("decimals"), 94 | "logo": response.get("logo"), 95 | "thumbnail": response.get("thumbnail"), 96 | "total_supply": response.get("total_supply"), 97 | "total_supply_formatted": response.get("total_supply_formatted"), 98 | "fully_diluted_valuation": response.get("fully_diluted_valuation"), 99 | "block_number": response.get("block_number"), 100 | "validated": response.get("validated"), 101 | "created_at": response.get("created_at"), 102 | "possible_spam": response.get("possible_spam", False), 103 | "verified_contract": response.get("verified_contract", False), 104 | "categories": response.get("categories", []), 105 | "links": response.get("links", {}), 106 | "security_score": response.get("security_score"), 107 | "description": response.get("description"), 108 | "circulating_supply": response.get("circulating_supply"), 109 | "market_cap": response.get("market_cap") 110 | } 111 | 112 | return metadata 113 | 114 | except Exception as e: 115 | logging.error(f"Error getting token metadata: {e}", exc_info=True) 116 | return None 117 | 118 | async def get_token_details(token_address: str, chain: str) -> Dict[str, Any]: 119 | """ 120 | Get detailed token details for a specific token 121 | 122 | Args: 123 | token_address: The token contract address 124 | chain: The blockchain network (eth, base, bsc) 125 | 126 | Returns: 127 | Dictionary containing token details including name, symbol, decimals, 128 | total supply, logo, links, categories, etc. 129 | """ 130 | logging.info(f"Getting token details for {token_address} on {chain}") 131 | 132 | try: 133 | # Fetch token details from API 134 | response = await fetch_token_details(chain, token_address) 135 | 136 | if not response: 137 | logging.warning(f"No details found for token {token_address} on {chain}") 138 | return None 139 | 140 | return response 141 | 142 | except Exception as e: 143 | logging.error(f"Error getting token details: {e}", exc_info=True) 144 | return None 145 | 146 | async def get_token_first_buyers(token_address: str, chain: str) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]: 147 | """ 148 | Get the first buyers data for a specific token 149 | 150 | Args: 151 | token_address: The token contract address 152 | chain: The blockchain network 153 | 154 | Returns: 155 | Tuple of (deployer_info, first_buyers) 156 | """ 157 | logging.info(f"Getting first buyers for {token_address} on {chain}") 158 | 159 | response = await fetch_first_buyers(chain, token_address) 160 | 161 | deployer_info = response.get("deployer_info") 162 | first_buyers = response.get("unique_buyers") 163 | 164 | return deployer_info, first_buyers[:10] 165 | 166 | async def get_token_profitable_wallets(token_address: str, chain: str) -> List[Dict[str, Any]]: 167 | """ 168 | Get the most profitable wallets for a specific token 169 | 170 | Args: 171 | token_address: The token contract address 172 | chain: The blockchain network 173 | 174 | Returns: 175 | List of dictionaries containing profitable wallet data 176 | """ 177 | logging.info(f"Getting profitable wallets for {token_address} on {chain}") 178 | 179 | # Fetch data from API 180 | response = await fetch_token_profitable_wallets(chain, token_address) 181 | 182 | profitable_wallets = response.get("traders") 183 | 184 | return profitable_wallets 185 | 186 | async def get_ath_data(token_address: str, chain: str) -> Dict[str, Any]: 187 | """ 188 | Get the ATH data for a specific token 189 | 190 | Args: 191 | token_address: The token contract address 192 | chain: The blockchain network 193 | 194 | Returns: 195 | Dictionary containing token ATH data 196 | """ 197 | logging.info(f"Getting ATH data for {token_address} on {chain}") 198 | 199 | response = await fetch_market_cap(chain, token_address.lower()) 200 | 201 | # Extract all fields from the response 202 | age_candles = response.get("age_candles", 0) 203 | age_days = response.get("age_days", 0) 204 | current_mc = response.get("current_mc", 0) 205 | current_mc_formatted = response.get("current_mc_formatted", "$0") 206 | starting_mc = response.get("starting_mc", 0) 207 | starting_mc_formatted = response.get("starting_mc_formatted", "$0") 208 | ath_mc = response.get("ath_mc", 0) 209 | ath_mc_formatted = response.get("ath_mc_formatted", "$0") 210 | ath_date = response.get("ath_date", "") 211 | deployment_date = response.get("deployment_date", "") 212 | x_start = response.get("x_start", 0) 213 | x_start_formatted = response.get("x_start_formatted", "N/A") 214 | x_current = response.get("x_current", 0) 215 | x_current_formatted = response.get("x_current_formatted", "N/A") 216 | 217 | # Create ATH data dictionary with all available information 218 | ath_data = { 219 | "age_candles": age_candles, 220 | "age_days": age_days, 221 | "current_mc": current_mc, 222 | "current_mc_formatted": current_mc_formatted, 223 | "starting_mc": starting_mc, 224 | "starting_mc_formatted": starting_mc_formatted, 225 | "ath_mc": ath_mc, 226 | "ath_mc_formatted": ath_mc_formatted, 227 | "ath_date": ath_date, 228 | "deployment_date": deployment_date, 229 | "x_start": x_start, 230 | "x_start_formatted": x_start_formatted, 231 | "x_current": x_current, 232 | "x_current_formatted": x_current_formatted, 233 | } 234 | 235 | return ath_data 236 | 237 | async def get_deployer_wallet_scan_data(token_address: str, chain: str) -> Dict[str, Any]: 238 | """ 239 | Get deployer wallet data for a specific token 240 | 241 | Args: 242 | token_address: The token contract address 243 | chain: The blockchain network (eth, base, bsc) 244 | 245 | Returns: 246 | Dictionary containing deployer wallet data 247 | """ 248 | logging.info(f"Getting deployer wallet scan data for {token_address} on {chain}") 249 | 250 | from utils import get_token_info 251 | 252 | try: 253 | # Fetch token deployer projects data from API 254 | response = await fetch_token_deployer_projects(chain, token_address) 255 | logging.info(f"Deployer wallet scan data fetched successfully for {token_address} on {chain}") 256 | 257 | if not response: 258 | logging.warning(f"No deployer data found for token {token_address} on {chain}") 259 | return None 260 | 261 | # Extract data from response 262 | token_address = response.get("token_address") 263 | deployer_address = response.get("deployer_address") 264 | chain_name = response.get("chain") 265 | related_tokens = response.get("related_tokens", []) 266 | total_count = response.get("total_count", 0) + 1 267 | 268 | # Create tasks for parallel processing 269 | token_info_tasks = [] 270 | market_cap_tasks = [] 271 | contract_addresses = [] 272 | 273 | # Prepare tasks for all tokens 274 | for token in related_tokens: 275 | contract_address = token.get("token_address") 276 | contract_addresses.append(contract_address) 277 | 278 | # Create tasks for parallel execution 279 | token_info_tasks.append(get_token_info(contract_address, chain)) 280 | market_cap_tasks.append(fetch_market_cap(chain, contract_address)) 281 | 282 | # Execute all tasks in parallel 283 | import asyncio 284 | token_info_results, market_cap_results = await asyncio.gather( 285 | asyncio.gather(*token_info_tasks), 286 | asyncio.gather(*market_cap_tasks) 287 | ) 288 | 289 | # Process related tokens data 290 | deployed_tokens = [] 291 | for i, token in enumerate(related_tokens): 292 | contract_address = contract_addresses[i] 293 | token_info = token_info_results[i] 294 | market_cap_data = market_cap_results[i] 295 | 296 | # Skip if we couldn't get market cap data 297 | if not market_cap_data: 298 | continue 299 | 300 | # Extract market cap information 301 | current_mc = market_cap_data.get("current_mc", 0) 302 | ath_mc = market_cap_data.get("ath_mc", 0) 303 | ath_date = market_cap_data.get("ath_date", "") 304 | 305 | # Create token data object with available information 306 | token_data = { 307 | "address": contract_address, 308 | "name": token_info.get("name", "Unknown Token") if token_info else "Unknown Token", 309 | "symbol": token_info.get("symbol", "???") if token_info else "???", 310 | "deploy_date": token.get("deployment_time_readable", "").split("T")[0], 311 | "current_market_cap": current_mc, 312 | "ath_market_cap": ath_mc, 313 | "ath_date": ath_date, 314 | "deployment_tx": token.get("transaction_hash"), 315 | } 316 | 317 | # Calculate x-multiplier if we have both current and ATH market caps 318 | if current_mc and ath_mc and ath_mc > 0: 319 | token_data["x_multiplier"] = f"{round(ath_mc / current_mc, 2)}x" 320 | else: 321 | token_data["x_multiplier"] = "0" 322 | 323 | deployed_tokens.append(token_data) 324 | 325 | # Sort by deploy date (newest first) 326 | deployed_tokens.sort(key=lambda x: x.get("deploy_date", ""), reverse=True) 327 | 328 | # Create deployer wallet data 329 | deployer_data = { 330 | "deployer_address": deployer_address, 331 | "tokens_deployed": total_count, 332 | "deployed_tokens": deployed_tokens 333 | } 334 | 335 | return deployer_data 336 | 337 | except Exception as e: 338 | logging.error(f"Error getting deployer wallet scan data: {e}") 339 | return None 340 | 341 | async def get_token_top_holders(token_address: str, chain: str) -> List[Dict[str, Any]]: 342 | """ 343 | Get top holders data for a specific token 344 | 345 | Args: 346 | token_address: The token contract address 347 | chain: The blockchain network 348 | 349 | Returns: 350 | List of dictionaries containing top holder data 351 | """ 352 | logging.info(f"Getting top holders for {token_address} on {chain}") 353 | 354 | # Fetch data from API or service 355 | response = await fetch_token_holders(chain, token_address) 356 | 357 | top_holders = [] 358 | 359 | # Process the response data 360 | for i, holder in enumerate(response[:10], 1): # Limit to top 10 holders 361 | logging.info(f"Processing holder {i}: {holder}") 362 | top_holders.append({ 363 | "rank": i, 364 | "address": holder.get('address', ''), 365 | "token_amount": holder.get('amount_cur', 0), 366 | "amount_readable": holder.get('amount_readable', 'N/A'), 367 | "percentage": holder.get('amount_percentage', 0) * 100, 368 | "percentage_readable": holder.get('percentage_readable', 'N/A'), 369 | "usd_value": holder.get('usd_value', 0), 370 | "usd_value_readable": holder.get('usd_value_readable', 'N/A'), 371 | "holding_since": holder.get('holding_since', 'N/A'), 372 | "percentage_readable": holder.get('percentage_readable', 'N/A') 373 | }) 374 | 375 | return top_holders 376 | 377 | async def get_high_net_worth_holders(token_address: str, chain: str) -> List[Dict[str, Any]]: 378 | """ 379 | Get high net worth holders data for a specific token 380 | 381 | Args: 382 | token_address: The token contract address 383 | chain: The blockchain network (eth, base, bsc) 384 | 385 | Returns: 386 | List of dictionaries containing high net worth holder data 387 | """ 388 | logging.info(f"Getting high net worth holders for {token_address} on {chain}") 389 | 390 | try: 391 | # Fetch data from API 392 | response = await fetch_high_net_worth_holders(chain, token_address) 393 | logging.info(f"response for fetching high_net_worth_holders for {token_address} on {chain}----------> {response}") 394 | 395 | # Check if response is valid (should be a list) 396 | if not response or not isinstance(response, list): 397 | logging.warning(f"Invalid response format for high net worth holders: {response}") 398 | return [] 399 | 400 | # Process the response data 401 | high_net_worth_holders = [] 402 | for holder in response: 403 | # Ensure holder is a dictionary 404 | if not isinstance(holder, dict): 405 | logging.warning(f"Invalid holder data format: {holder}") 406 | continue 407 | 408 | # Extract wallet address 409 | address = holder.get('address', '') 410 | 411 | # Extract token amount and value 412 | token_amount = holder.get('amount_cur', 0) 413 | 414 | # Store both the original string value and a numerical value for sorting 415 | usd_value_str = holder.get('usd_value', '$0') 416 | 417 | # Convert string USD value to a numerical value for sorting 418 | usd_value_numeric = 0 419 | if isinstance(usd_value_str, str) and usd_value_str.startswith('$'): 420 | try: 421 | # Remove $ and any commas, then convert to float 422 | usd_value_numeric = float(usd_value_str.replace('$', '').replace(',', '')) 423 | except ValueError: 424 | logging.warning(f"Could not convert USD value to float: {usd_value_str}") 425 | elif isinstance(usd_value_str, (int, float)): 426 | usd_value_numeric = float(usd_value_str) 427 | 428 | # Get portfolio information 429 | portfolio_size = holder.get('portfolio_count', len(holder.get('top_holdings', []))) 430 | total_portfolio_value = holder.get('total_usd_value', 0) 431 | total_usd_value_excluding_token_formatted = holder.get('total_usd_value_excluding_token_formatted', '$0') 432 | 433 | # Extract top holdings 434 | top_holdings = [] 435 | for holding in holder.get('top_holdings', []): 436 | top_holdings.append({ 437 | 'symbol': holding.get('symbol', 'Unknown'), 438 | 'value': holding.get('value_formatted', '$0'), 439 | 'url': holding.get('explorer_url', '') 440 | }) 441 | 442 | last_active_time = "N/A" 443 | if holder.get('last_active_timestamp'): 444 | try: 445 | last_active_time = datetime.fromtimestamp(holder['last_active_timestamp']).strftime("%Y-%m-%d") 446 | except: 447 | pass 448 | 449 | # Create holder data object 450 | holder_data = { 451 | "address": address, 452 | "percentage": holder.get('amount_percentage', 0) * 100, 453 | "token_amount": token_amount, 454 | "usd_value": usd_value_str, 455 | "usd_value_numeric": usd_value_numeric, 456 | "total_portfolio_value": total_portfolio_value, 457 | "total_usd_value_excluding_token_formatted": total_usd_value_excluding_token_formatted, 458 | "portfolio_size": portfolio_size, 459 | "top_holdings": top_holdings[:10], 460 | "last_active_time": last_active_time, 461 | } 462 | 463 | high_net_worth_holders.append(holder_data) 464 | 465 | # Sort by numeric USD value (highest first) 466 | high_net_worth_holders.sort(key=lambda x: x["usd_value_numeric"], reverse=True) 467 | 468 | return high_net_worth_holders 469 | 470 | except Exception as e: 471 | logging.error(f"Error getting high net worth holders: {e}", exc_info=True) 472 | return [] 473 | -------------------------------------------------------------------------------- /src/database/wallet_operations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Database operations related to wallets. 3 | """ 4 | 5 | import logging 6 | from typing import Dict, List, Any, Optional 7 | from datetime import datetime, timedelta 8 | 9 | from models import WalletData 10 | from api.token_api import * 11 | from api.wallet_api import * 12 | from .core import get_database 13 | 14 | def save_wallet_data(wallet: WalletData) -> None: 15 | """ 16 | Save or update wallet data 17 | 18 | Args: 19 | wallet: The wallet data object to save 20 | """ 21 | db = get_database() 22 | wallet_dict = wallet.to_dict() 23 | wallet_dict["address"] = wallet_dict["address"].lower() # Normalize address 24 | wallet_dict["last_updated"] = datetime.now() 25 | 26 | db.wallet_data.update_one( 27 | {"address": wallet_dict["address"]}, 28 | {"$set": wallet_dict}, 29 | upsert=True 30 | ) 31 | 32 | def get_profitable_wallets(days: int, limit: int = 10) -> List[Dict[str, Any]]: 33 | """ 34 | Get most profitable wallets in the last N days 35 | 36 | Args: 37 | days: Number of days to look back 38 | limit: Maximum number of wallets to return 39 | 40 | Returns: 41 | List of wallet data dictionaries 42 | """ 43 | db = get_database() 44 | since_date = datetime.now() - timedelta(days=days) 45 | 46 | # This is a placeholder - in a real implementation, you would have a collection 47 | # of wallet transactions or profits to query from 48 | wallets = db.wallet_data.find({ 49 | "last_updated": {"$gte": since_date}, 50 | "win_rate": {"$gt": 50} # Only wallets with >50% win rate 51 | }).sort("win_rate", -1).limit(limit) 52 | 53 | return [WalletData.from_dict(wallet).to_dict() for wallet in wallets] 54 | 55 | def get_profitable_deployers(days: int, limit: int = 10) -> List[Dict[str, Any]]: 56 | """ 57 | Get most profitable token deployer wallets in the last N days 58 | 59 | Args: 60 | days: Number of days to look back 61 | limit: Maximum number of wallets to return 62 | 63 | Returns: 64 | List of wallet data dictionaries 65 | """ 66 | db = get_database() 67 | since_date = datetime.now() - timedelta(days=days) 68 | 69 | # This is a placeholder - in a real implementation, you would have more complex logic 70 | wallets = db.wallet_data.find({ 71 | "is_deployer": True, 72 | "last_updated": {"$gte": since_date} 73 | }).sort("win_rate", -1).limit(limit) 74 | 75 | return [WalletData.from_dict(wallet).to_dict() for wallet in wallets] 76 | 77 | async def get_wallet_most_profitable_in_period(days: int = 30, limit: int = 10, chain: str = "eth") -> List[Dict[str, Any]]: 78 | """ 79 | Get the most profitable wallets in a specific period 80 | 81 | Args: 82 | days: Number of days to look back 83 | limit: Maximum number of wallets to return 84 | chain: The blockchain network (eth, base, bsc) 85 | 86 | Returns: 87 | List of dictionaries containing wallet data 88 | """ 89 | logging.info(f"Getting most profitable wallets for {days} days, limit {limit}, chain {chain}") 90 | 91 | try: 92 | response = await fetch_profitable_defi_wallets(chain) 93 | 94 | # Check if we have data 95 | if not response or "periods" not in response: 96 | logging.warning(f"No data returned from fetch_profitable_defi_wallets for chain {chain}") 97 | return [] 98 | 99 | # Find the period that matches the requested days 100 | wallets = [] 101 | for period in response.get("periods", []): 102 | if period.get("days") == days: 103 | wallets = period.get("wallets", []) 104 | break 105 | 106 | # If no matching period found or no wallets in that period 107 | if not wallets: 108 | logging.info(f"No wallets found for period {days} days on chain {chain}") 109 | return [] 110 | 111 | # Transform the data to match our expected format 112 | formatted_wallets = [] 113 | for wallet in wallets[:limit]: 114 | formatted_wallets.append({ 115 | "address": wallet.get("wallet_address"), 116 | "total_profit": wallet.get("total_profit", 0), 117 | "total_profit_readable": wallet.get("total_profit_readable", 'N/A'), 118 | "total_unrealized": wallet.get("total_unrealized", 0), 119 | "total_unrealized_readable": wallet.get("total_unrealized_readable", 'N/A'), 120 | "win_rate": wallet.get("win_rate", 0) * 100, 121 | "total_trades": wallet.get("total_trades", 0), 122 | "total_buy_trades": wallet.get("total_buy_trades", 0), 123 | "total_sell_trades": wallet.get("total_sell_trades", 0), 124 | "period_days": days, 125 | "chain": chain, 126 | "total_buy_usd": wallet.get("total_buy_usd", 0), 127 | "total_buy_usd_readable": wallet.get("total_buy_usd_readable", 'N/A'), 128 | "total_sell_usd": wallet.get("total_sell_usd", 0), 129 | "total_sell_usd_readable": wallet.get("total_sell_usd_readable", 'N/A'), 130 | "total_wins": wallet.get("total_wins", 0), 131 | "total_losses": wallet.get("total_losses", 0), 132 | "pnl_ratio": wallet.get("pnl_ratio", 0), 133 | "avg_hold_hours": wallet.get("avg_hold_hours", 0), 134 | "avg_hold_days": wallet.get("avg_hold_days", 0), 135 | }) 136 | 137 | logging.info(f"Returning {len(formatted_wallets)} wallets") 138 | return formatted_wallets 139 | 140 | except Exception as e: 141 | logging.error(f"Error getting most profitable wallets: {e}", exc_info=True) 142 | return [] 143 | 144 | async def get_most_profitable_token_deployer_wallets(days: int = 30, limit: int = 10, chain: str = "eth") -> list: 145 | """ 146 | Get most profitable token deployer wallets 147 | 148 | Args: 149 | days: Number of days to analyze 150 | limit: Maximum number of wallets to return 151 | chain: The blockchain network (eth, base, bsc) 152 | 153 | Returns: 154 | List of dictionaries containing profitable deployer wallet data 155 | """ 156 | logging.info(f"Getting most profitable token deployer wallets for {days} days on {chain}") 157 | 158 | try: 159 | # Fetch data from API 160 | response = await fetch_profitable_deployers(chain) 161 | logging.info(f"Response from API of get_most_profitable_token_deployer_wallets: {response}") 162 | 163 | # Check if we have data 164 | if not response or "periods" not in response: 165 | logging.warning(f"No data returned from fetch_profitable_deployer_wallets for chain {chain}") 166 | return [] 167 | 168 | # Find the period that matches the requested days 169 | wallets = [] 170 | for period in response.get("periods", []): 171 | if period.get("days") == days: 172 | wallets = period.get("wallets", []) 173 | break 174 | 175 | # If no matching period found or no wallets in that period 176 | if not wallets: 177 | logging.info(f"No deployer wallets found for period {days} days on chain {chain}") 178 | return [] 179 | 180 | # Transform the data to match our expected format 181 | formatted_wallets = [] 182 | for wallet in wallets[:limit]: 183 | formatted_wallets.append({ 184 | "address": wallet.get("wallet_address"), 185 | "successful_tokens": wallet.get("total_wins", 0), 186 | "total_profit": wallet.get("total_profit", "0"), 187 | "chain": chain, 188 | "period_days": days, 189 | "total_buy_usd": wallet.get("total_buy_usd", "0"), 190 | "total_sell_usd": wallet.get("total_sell_usd", "0"), 191 | "total_trades": wallet.get("total_trades", 0), 192 | "total_wins": wallet.get("total_wins", 0), 193 | "total_losses": wallet.get("total_losses", 0), 194 | "win_rate": wallet.get("win_rate", 0) 195 | }) 196 | 197 | logging.info(f"Returning {len(formatted_wallets)} deployer wallets") 198 | return formatted_wallets 199 | 200 | except Exception as e: 201 | logging.error(f"Error getting most profitable token deployer wallets: {e}", exc_info=True) 202 | return [] 203 | 204 | async def get_wallet_holding_duration(wallet_address: str, chain: str = "eth") -> dict: 205 | """ 206 | Get holding duration data for a wallet 207 | 208 | Args: 209 | wallet_address: The wallet address to analyze 210 | chain: The blockchain network (eth, base, bsc) 211 | 212 | Returns: 213 | Dictionary containing holding duration data 214 | """ 215 | logging.info(f"Getting holding duration data for wallet {wallet_address} on {chain}") 216 | 217 | try: 218 | # Fetch data from API 219 | response = await fetch_wallet_holding_time(chain, wallet_address) 220 | 221 | if not response or "wallet_address" not in response: 222 | logging.warning(f"No holding duration data found for wallet {wallet_address} on {chain}") 223 | return { 224 | "wallet_address": wallet_address, 225 | "chain": chain, 226 | "error": "No holding duration data available" 227 | } 228 | 229 | # Extract holding times data 230 | holding_times = response.get("holding_times", {}) 231 | tokens_info = response.get("tokens", {}) 232 | total_tokens = response.get("total_tokens", 0) 233 | 234 | # Get shortest and longest hold token info 235 | shortest_hold_token = tokens_info.get("shortest_hold", {}) 236 | longest_hold_token = tokens_info.get("longest_hold", {}) 237 | 238 | # Create holding duration data object 239 | holding_data = { 240 | "wallet_address": wallet_address, 241 | "chain": chain, 242 | "total_tokens_analyzed": total_tokens, 243 | "avg_holding_time_days": holding_times.get("average", {}).get("formatted", "N/A"), 244 | "shortest_holding_time": holding_times.get("shortest", {}).get("formatted", "N/A"), 245 | "longest_holding_time": holding_times.get("longest", {}).get("formatted", "N/A"), 246 | "shortest_hold_token": { 247 | "address": shortest_hold_token.get("address", "N/A"), 248 | "symbol": shortest_hold_token.get("symbol", "Unknown") 249 | }, 250 | "longest_hold_token": { 251 | "address": longest_hold_token.get("address", "N/A"), 252 | "symbol": longest_hold_token.get("symbol", "Unknown") 253 | } 254 | } 255 | 256 | logging.info(f"Successfully retrieved holding duration data for wallet {wallet_address}") 257 | return holding_data 258 | 259 | except Exception as e: 260 | logging.error(f"Error getting wallet holding duration: {e}") 261 | return { 262 | "wallet_address": wallet_address, 263 | "chain": chain, 264 | "error": f"Failed to retrieve holding duration data: {str(e)}" 265 | } 266 | 267 | async def get_tokens_deployed_by_wallet(wallet_address: str, chain: str = "eth") -> list: 268 | """ 269 | Get tokens deployed by a wallet 270 | 271 | Args: 272 | wallet_address: The wallet address to analyze 273 | chain: The blockchain network (eth, base, bsc) 274 | 275 | Returns: 276 | List of dictionaries containing token data 277 | """ 278 | logging.info(f"Getting tokens deployed by wallet {wallet_address} on {chain}") 279 | 280 | from utils import get_token_info 281 | import asyncio 282 | 283 | try: 284 | # Fetch data from API 285 | response = await fetch_wallet_deployed_tokens(chain, wallet_address) 286 | 287 | if not response or "tokens_deployed" not in response: 288 | logging.warning(f"No deployed tokens found for wallet {wallet_address} on {chain}") 289 | return [] 290 | 291 | # Extract deployed tokens from response 292 | deployed_tokens_raw = response.get("tokens_deployed", []) 293 | total_count = response.get("total_count", 0) 294 | 295 | # Create tasks for parallel processing 296 | token_info_tasks = [] 297 | market_cap_tasks = [] 298 | contract_addresses = [] 299 | 300 | # Prepare all tasks for parallel execution 301 | for token in deployed_tokens_raw: 302 | contract_address = token.get("token_address") 303 | contract_addresses.append(contract_address) 304 | 305 | # Create tasks for token info and market cap data 306 | token_info_tasks.append(get_token_info(contract_address, chain)) 307 | market_cap_tasks.append(fetch_market_cap(chain, contract_address)) 308 | 309 | # Execute all tasks in parallel 310 | token_info_results, market_cap_results = await asyncio.gather( 311 | asyncio.gather(*token_info_tasks), 312 | asyncio.gather(*market_cap_tasks) 313 | ) 314 | 315 | # Process results 316 | tokens = [] 317 | for i, token in enumerate(deployed_tokens_raw): 318 | contract_address = contract_addresses[i] 319 | token_info = token_info_results[i] 320 | market_cap_data = market_cap_results[i] 321 | 322 | # Skip tokens with no ATH market cap 323 | if not market_cap_data or market_cap_data.get("ath_mc", 0) == 0: 324 | continue 325 | 326 | # Extract market cap information 327 | current_mc = market_cap_data.get("current_mc", 0) if market_cap_data else 0 328 | current_mc_formatted = market_cap_data.get("current_mc_formatted", 0) if market_cap_data else 0 329 | ath_mc = market_cap_data.get("ath_mc", 0) if market_cap_data else 0 330 | ath_mc_formatted = market_cap_data.get("ath_mc_formatted", 0) if market_cap_data else 0 331 | ath_date = market_cap_data.get("ath_date", "") if market_cap_data else "" 332 | deployment_date = market_cap_data.get("deployment_date", "") if market_cap_data else "N/A" 333 | x_current_formatted = market_cap_data.get("x_current_formatted", 0) if market_cap_data else 0 334 | 335 | # Create token data object with available information 336 | token_data = { 337 | "address": contract_address, 338 | "name": token_info.get("name", "Unknown Token") if token_info else "Unknown Token", 339 | "symbol": token_info.get("symbol", "???") if token_info else "???", 340 | "deployment_date": deployment_date, 341 | "current_market_cap": current_mc, 342 | "current_mc_formatted": current_mc_formatted, 343 | "ath_market_cap": ath_mc, 344 | "ath_mc_formatted": ath_mc_formatted, 345 | "ath_date": ath_date, 346 | "deployment_tx": token.get("transaction_hash"), 347 | "x_current_formatted": x_current_formatted, 348 | } 349 | 350 | tokens.append(token_data) 351 | 352 | # Sort by deploy date (newest first) 353 | tokens.sort(key=lambda x: x.get("deployment_date", "") or "", reverse=False) 354 | 355 | logging.info(f"Found {len(tokens)} tokens deployed by wallet {wallet_address}") 356 | return tokens 357 | 358 | except Exception as e: 359 | logging.error(f"Error getting tokens deployed by wallet: {e}") 360 | return [] 361 | 362 | async def get_kol_wallet_profitability(days: int = 7, limit: int = 10, chain: str = "eth", kol_name: str = None) -> list: 363 | """ 364 | Get KOL wallet profitability data 365 | 366 | Args: 367 | days: Number of days to analyze (1, 7, or 30) 368 | limit: Maximum number of results to return 369 | chain: Blockchain to analyze (eth, base, bsc) 370 | kol_name: Name of the specific KOL to filter by (optional) 371 | 372 | Returns: 373 | List of KOL wallet profitability data 374 | """ 375 | logging.info(f"Getting KOL wallet profitability for {days} days on {chain}") 376 | 377 | try: 378 | # Determine the order_by parameter based on days 379 | order_by = f"pnl_{days}d" if days in [1, 7, 30] else "pnl_7d" 380 | 381 | # Fetch data from API 382 | response = await fetch_kol_wallets(chain, order_by) 383 | 384 | if not response or "wallets" not in response: 385 | logging.warning(f"No KOL wallet data found for chain {chain}") 386 | return [] 387 | 388 | # Extract wallets from response 389 | wallets = response.get("wallets", []) 390 | 391 | # Filter by KOL name if provided 392 | if kol_name: 393 | filtered_wallets = [] 394 | for w in wallets: 395 | name = w.get("name") 396 | name = name.replace(" ", "") if name is not None else "" 397 | twitter_name = w.get("twitter_name") 398 | twitter_name = twitter_name.replace(" ", "") if twitter_name is not None else "" 399 | ens = w.get("ens") 400 | ens = ens.replace(" ", "") if ens is not None else "" 401 | 402 | if (name.lower() == kol_name.lower() or 403 | twitter_name.lower() == kol_name.lower() or 404 | ens.lower() == kol_name.lower()): 405 | filtered_wallets.append(w) 406 | 407 | elif (kol_name.lower() in name.lower() or 408 | kol_name.lower() in twitter_name.lower() or 409 | kol_name.lower() in ens.lower()): 410 | filtered_wallets.append(w) 411 | 412 | elif (name.lower() and name.lower() in kol_name.lower() or 413 | twitter_name.lower() and twitter_name.lower() in kol_name.lower() or 414 | ens.lower() and ens.lower() in kol_name.lower()): 415 | filtered_wallets.append(w) 416 | 417 | wallets = filtered_wallets 418 | 419 | # Format the wallet data 420 | formatted_wallets = [] 421 | for wallet in wallets[:limit]: 422 | # Get the appropriate profit field based on days 423 | profit_field = f"realized_profit_{days}d" if days in [1, 7, 30] else "realized_profit_7d" 424 | pnl_field = f"pnl_{days}d" if days in [1, 7, 30] else "pnl_7d" 425 | 426 | formatted_wallet = { 427 | "address": wallet.get("wallet_address", wallet.get("address", "")), 428 | "name": wallet.get("name", "Unknown"), 429 | "twitter": wallet.get("twitter_username", ""), 430 | "ens": wallet.get("ens", ""), 431 | "followers": wallet.get("followers_count", 0), 432 | "profit": wallet.get(profit_field, 0), 433 | "pnl": wallet.get(pnl_field, 0), 434 | "win_rate": wallet.get(f"winrate_{days}d", 0) if days in [7, 30] else wallet.get("winrate_7d", 0), 435 | "transactions": wallet.get("txs", 0), 436 | "tokens_traded": wallet.get(f"token_num_{days}d", 0) if days in [7, 30] else wallet.get("token_num_7d", 0), 437 | "avg_holding_time": wallet.get(f"avg_holding_period_{days}d", 0) if days in [7, 30] else wallet.get("avg_holding_period_7d", 0), 438 | "last_active": wallet.get("last_active_readable", ""), 439 | "avatar": wallet.get("avatar", ""), 440 | "chain": chain, 441 | "period": days 442 | } 443 | 444 | formatted_wallets.append(formatted_wallet) 445 | 446 | # Sort by profit (descending) 447 | formatted_wallets.sort(key=lambda x: x["profit"], reverse=True) 448 | 449 | logging.info(f"Returning {len(formatted_wallets)} KOL wallets") 450 | return formatted_wallets 451 | 452 | except Exception as e: 453 | logging.error(f"Error getting KOL wallet profitability: {e}", exc_info=True) 454 | return [] 455 | 456 | -------------------------------------------------------------------------------- /src/utils/wallet_analysis.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Dict, Any, List, Tuple, Optional 3 | from datetime import datetime, timedelta 4 | 5 | from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup 6 | from telegram.ext import ContextTypes 7 | from telegram.constants import ParseMode 8 | 9 | from models import User 10 | from config.settings import FREE_WALLET_SCANS_DAILY 11 | from services.blockchain_service import is_valid_wallet_address 12 | from services.user_service import get_user, increment_scan_count, check_rate_limit 13 | from utils.user_utils import check_callback_user 14 | from utils.formatting import format_number 15 | 16 | async def handle_wallet_analysis_input( 17 | update: Update, 18 | context: ContextTypes.DEFAULT_TYPE, 19 | analysis_type: str, 20 | get_data_func, 21 | format_response_func, 22 | scan_count_type: str, 23 | processing_message_text: str, 24 | error_message_text: str, 25 | no_data_message_text: str, 26 | ) -> None: 27 | """ 28 | Generic handler for wallet analysis inputs 29 | 30 | Args: 31 | update: The update object 32 | context: The context object 33 | analysis_type: Type of analysis being performed (for logging) 34 | get_data_func: Function to get the data (takes wallet_address) 35 | format_response_func: Function to format the response (takes data) 36 | scan_count_type: Type of scan to increment count for 37 | processing_message_text: Text to show while processing 38 | error_message_text: Text to show on error 39 | no_data_message_text: Text to show when no data is found 40 | additional_params: Additional parameters to pass to get_data_func 41 | """ 42 | wallet_address = update.message.text.strip() 43 | selected_chain = context.user_data.get("selected_chain", "eth") 44 | 45 | if not await is_valid_wallet_address(wallet_address, selected_chain): 46 | keyboard = [[InlineKeyboardButton("🔙 Back", callback_data="wallet_analysis")]] 47 | reply_markup = InlineKeyboardMarkup(keyboard) 48 | 49 | await update.message.reply_text( 50 | f"⚠️ Something went wrong.⚠️ Please provide a valid wallet address on {selected_chain}.", 51 | reply_markup=reply_markup 52 | ) 53 | return 54 | 55 | # Send processing message 56 | processing_message = await update.message.reply_text(processing_message_text) 57 | try: 58 | # Get data 59 | data = await get_data_func(wallet_address, selected_chain) 60 | 61 | if not data: 62 | # Add back button when no data is found 63 | keyboard = [[InlineKeyboardButton("🔙 Back", callback_data="wallet_analysis")]] 64 | reply_markup = InlineKeyboardMarkup(keyboard) 65 | 66 | await processing_message.edit_text( 67 | no_data_message_text, 68 | reply_markup=reply_markup 69 | ) 70 | return 71 | 72 | # Format the response 73 | response, keyboard = format_response_func(data, wallet_address) 74 | reply_markup = InlineKeyboardMarkup(keyboard) 75 | 76 | success = False 77 | try: 78 | # Try to edit the current message 79 | await processing_message.edit_text( 80 | response, 81 | reply_markup=reply_markup, 82 | parse_mode=ParseMode.HTML 83 | ) 84 | success = True 85 | except Exception as e: 86 | logging.error(f"Error in handle_{analysis_type}: {e}") 87 | # If editing fails, send a new message 88 | await update.message.reply_text( 89 | response, 90 | reply_markup=reply_markup, 91 | parse_mode=ParseMode.HTML 92 | ) 93 | success = True 94 | # Delete the original message if possible 95 | try: 96 | await processing_message.delete() 97 | except: 98 | pass 99 | 100 | # Only increment scan count if we successfully displayed data 101 | if success: 102 | # Get the user directly from the message update 103 | user_id = update.effective_user.id 104 | user = get_user(user_id) 105 | if not user: 106 | # Create user if not exists 107 | user = User(user_id=user_id, username=update.effective_user.username) 108 | # Save user to database if needed 109 | 110 | await increment_scan_count(user_id, scan_count_type) 111 | 112 | except Exception as e: 113 | logging.error(f"Error in handle_expected_input ({analysis_type}): {e}") 114 | 115 | # Add back button to exception error message 116 | keyboard = [[InlineKeyboardButton("🔙 Back", callback_data="wallet_analysis")]] 117 | reply_markup = InlineKeyboardMarkup(keyboard) 118 | 119 | await processing_message.edit_text( 120 | error_message_text, 121 | reply_markup=reply_markup 122 | ) 123 | 124 | def format_wallet_holding_duration_response(data: dict, wallet_address: str) -> tuple: 125 | """ 126 | Format the response for wallet holding duration analysis 127 | 128 | Args: 129 | data: Wallet holding duration data 130 | wallet_address: The wallet address 131 | 132 | Returns: 133 | Tuple of (formatted response text, keyboard buttons) 134 | """ 135 | # Check if there was an error 136 | if "error" in data: 137 | response = ( 138 | f"⏱️ Wallet Holding Duration Analysis\n\n" 139 | f"💼 {wallet_address}\n" 140 | f"🌐 Chain: {data.get('chain', 'ETH').upper()}\n\n" 141 | f"❌ Error: {data.get('error', 'Unknown error occurred')}\n\n" 142 | f"Please try again later or try a different wallet address." 143 | ) 144 | else: 145 | # Format the shortest and longest hold token info 146 | shortest_token = data.get('shortest_hold_token', {}) 147 | longest_token = data.get('longest_hold_token', {}) 148 | 149 | shortest_symbol = shortest_token.get('symbol') or "Unknown" 150 | longest_symbol = longest_token.get('symbol') or "Unknown" 151 | 152 | shortest_address = shortest_token.get('address', 'N/A') 153 | longest_address = longest_token.get('address', 'N/A') 154 | 155 | response = ( 156 | f"⏱️ Wallet Holding Duration Analysis\n\n" 157 | f"💼 {wallet_address}\n" 158 | f"🌐 Chain: {data.get('chain', 'ETH').upper()}\n\n" 159 | f"📊 Holding Time Statistics:\n" 160 | f"• ⏱️ Average: {data.get('avg_holding_time_days', 'N/A')}\n" 161 | f"• ⌛️ Shortest: {data.get('shortest_holding_time', 'N/A')}\n" 162 | f"• ⏳ Longest: {data.get('longest_holding_time', 'N/A')}\n\n" 163 | f"🔍 Tokens Analyzed: {data.get('total_tokens_analyzed', 'N/A')}\n\n" 164 | f"🏆 Notable Holdings:\n" 165 | f"• ⌛️ Shortest held: {shortest_symbol} ({shortest_address})\n" 166 | f" 📑 {shortest_address}\n" 167 | f"• ⏳ Longest held: {longest_symbol}\n" 168 | f" 📑 {longest_address}\n\n" 169 | ) 170 | 171 | keyboard = [[ 172 | InlineKeyboardButton("Track Wallet", callback_data=f"track_wbs_{wallet_address}"), 173 | InlineKeyboardButton("Deployed Tokens", callback_data=f"tokens_deployed_{wallet_address}") 174 | ]] 175 | 176 | return response, keyboard 177 | 178 | def format_wallet_most_profitable_response(data: list, wallet_address: str = None) -> tuple: 179 | """ 180 | Format the response for most profitable wallets analysis 181 | 182 | Args: 183 | data: List of profitable wallet data 184 | wallet_address: Not used for this function, but kept for consistency 185 | 186 | Returns: 187 | Tuple of (formatted response text, keyboard buttons) 188 | """ 189 | # Get the first wallet to extract period info 190 | first_wallet = data[0] if data else {} 191 | period_days = first_wallet.get('period_days', 30) 192 | chain = first_wallet.get('chain', 'eth').upper() 193 | 194 | response = ( 195 | f"💰 Most Profitable Wallets Over the Last {period_days} Days\n" 196 | f"🌐 Chain Analyzed: {chain}\n\n" 197 | f"📈 Below is a list of the most profitable wallets based on their transaction activity and earnings during this period. " 198 | f"These wallets have shown strong performance and smart trading behavior that contributed to significant gains. " 199 | f"Dive into the details to see who's leading the profit charts! 🚀💼\n\n" 200 | ) 201 | 202 | for i, wallet in enumerate(data[:10], 1): 203 | # Format win rate as percentage with one decimal place 204 | win_rate = wallet.get('win_rate', 0) 205 | if isinstance(win_rate, (int, float)): 206 | win_rate_formatted = f"{win_rate:.1f}%" 207 | else: 208 | win_rate_formatted = "N/A" 209 | 210 | # Get total wins and losses 211 | total_wins = wallet.get('total_wins', 'N/A') 212 | total_losses = wallet.get('total_losses', 'N/A') 213 | 214 | # Get buy and sell trade counts 215 | total_trades = wallet.get('total_trades', 0) 216 | total_buy_trades = wallet.get('total_buy_trades', 0) 217 | total_sell_trades = wallet.get('total_sell_trades', 0) 218 | 219 | # Get unrealized profit 220 | total_profit_readable = wallet.get('total_profit_readable', 'N/A') 221 | total_unrealized_readable = wallet.get('total_unrealized_readable', 'N/A') 222 | 223 | # Get average hold time 224 | avg_hold_days = wallet.get('avg_hold_days', 'N/A') 225 | if isinstance(avg_hold_days, (int, float)): 226 | avg_hold_time_display = f"{avg_hold_days:.1f} days" 227 | else: 228 | avg_hold_time_display = "N/A" 229 | 230 | response += ( 231 | f"{i}. {wallet['address']}\n" 232 | f" 💵 Profit: {total_profit_readable}\n" 233 | f" 💎 Unrealized Profit: {total_unrealized_readable}\n" 234 | f" 📊 Win Rate: {win_rate_formatted} ({total_wins}W/{total_losses}L)\n" 235 | f" 🔄 Trades: {total_trades} (B: {total_buy_trades} / S: {total_sell_trades})\n" 236 | f" ⏱️ Avg Hold Time: {avg_hold_time_display}\n\n" 237 | ) 238 | 239 | keyboard = [[ 240 | InlineKeyboardButton("1 Day", callback_data="wallet_most_profitable_in_period_1"), 241 | InlineKeyboardButton("7 Days", callback_data="wallet_most_profitable_in_period_7"), 242 | InlineKeyboardButton("30 Days", callback_data="wallet_most_profitable_in_period_30") 243 | ]] 244 | 245 | return response, keyboard 246 | 247 | def format_deployer_wallets_response(data: list, wallet_address: str = None) -> tuple: 248 | """ 249 | Format the response for most profitable token deployer wallets 250 | 251 | Args: 252 | data: List of profitable deployer wallet data 253 | wallet_address: Not used for this function, but kept for consistency 254 | 255 | Returns: 256 | Tuple of (formatted response text, keyboard buttons) 257 | """ 258 | # Get the first wallet to extract period info 259 | first_wallet = data[0] if data else {} 260 | period_days = first_wallet.get('period_days', 30) 261 | chain = first_wallet.get('chain', 'eth').upper() 262 | 263 | response = ( 264 | f"🧪 Most Profitable Token Deployer Wallets (Last {period_days} Days)\n" 265 | f"🔗 Chain: {chain}\n\n" 266 | f"🚀 These wallet addresses have been busy deploying tokens and cashing in big over the last {period_days} days. " 267 | f"They're not just developers — they're trendsetters, launching tokens that gain traction fast! 💸📊\n\n" 268 | f"🔥 Let's take a closer look at the top-performing deployers who are making serious moves in the ecosystem.\n\n" 269 | ) 270 | 271 | for i, wallet in enumerate(data[:10], 1): 272 | total_profit = wallet.get('total_profit', 'N/A') 273 | total_buy_usd = wallet.get('total_buy_usd', 'N/A') 274 | total_sell_usd = wallet.get('total_sell_usd', 'N/A') 275 | 276 | response += ( 277 | f"{i}. `{wallet['address']}`\n" 278 | f" 💰 Profit: {total_profit}\n" 279 | f" 📉 Buy Vol: {total_buy_usd} | 📈 Sell Vol: {total_sell_usd}\n\n" 280 | ) 281 | 282 | # Add details of tokens deployed by this wallet 283 | deployed_tokens = wallet.get('deployed_tokens', []) 284 | if deployed_tokens: 285 | response += f" Notable Tokens Deployed:\n" 286 | for j, token in enumerate(deployed_tokens[:3], 1): # Show top 3 tokens 287 | token_name = token.get('name', 'Unknown') 288 | token_symbol = token.get('symbol', 'N/A') 289 | token_address = token.get('address', 'N/A') 290 | total_supply = token.get('total_supply', 'N/A') 291 | start_mcap = token.get('initial_market_cap', 'N/A') 292 | current_mcap = token.get('current_market_cap', 'N/A') 293 | ath_mcap = token.get('ath_market_cap', 'N/A') 294 | 295 | # Calculate age in days 296 | deploy_date = token.get('deploy_date') 297 | age_days = 'N/A' 298 | if deploy_date: 299 | try: 300 | # Try to calculate age if deploy_date is available 301 | if isinstance(deploy_date, str): 302 | deploy_date = datetime.strptime(deploy_date, '%Y-%m-%d %H:%M:%S') 303 | age_days = (datetime.now() - deploy_date).days 304 | except: 305 | pass 306 | 307 | # Calculate X's made (multiple of initial investment) 308 | x_multiple = 'N/A' 309 | if isinstance(current_mcap, (int, float)) and isinstance(start_mcap, (int, float)) and start_mcap > 0: 310 | x_multiple = f"{current_mcap / start_mcap:.1f}x" 311 | 312 | holders_count = token.get('holders_count', 'N/A') 313 | 314 | response += ( 315 | f" {j}. {token_name} ({token_symbol})\n" 316 | f" Address: {token_address}\n" 317 | f" Supply: {format_number(total_supply)}\n" 318 | f" Initial MCap: ${format_number(start_mcap)}\n" 319 | f" Current MCap: ${format_number(current_mcap)}\n" 320 | f" ATH MCap: ${format_number(ath_mcap)}\n" 321 | f" Age: {age_days} days\n" 322 | f" X's Made: {x_multiple}\n" 323 | f" Holders: {format_number(holders_count)}\n\n" 324 | ) 325 | 326 | if len(deployed_tokens) > 3: 327 | response += f" + {len(deployed_tokens) - 3} more tokens\n\n" 328 | 329 | keyboard = [[ 330 | InlineKeyboardButton("1 Day", callback_data="most_profitable_token_deployer_1"), 331 | InlineKeyboardButton("7 Days", callback_data="most_profitable_token_deployer_7"), 332 | InlineKeyboardButton("30 Days", callback_data="most_profitable_token_deployer_30") 333 | ]] 334 | 335 | return response, keyboard 336 | 337 | def format_tokens_deployed_response(data: list, wallet_address: str) -> tuple: 338 | """ 339 | Format the response for tokens deployed by wallet 340 | 341 | Args: 342 | data: List of token data 343 | wallet_address: The wallet address 344 | 345 | Returns: 346 | Tuple of (formatted response text, keyboard buttons) 347 | """ 348 | chain = data[0].get('chain', 'eth').upper() if data else 'ETH' 349 | 350 | response = ( 351 | f"🚀 Tokens Deployed by Wallet\n\n" 352 | f"👤 Deployer: {wallet_address}\n" 353 | f"🌐 Chain: {chain}\n" 354 | f"🧬 Total Tokens Deployed: {len(data)}\n\n" 355 | f"🔍 This wallet has been actively creating tokens on {chain}, possibly experimenting, launching new projects, or fueling DeFi/NFT ecosystems. " 356 | f"Whether it's for innovation or hype, it's clearly making moves! 💼📈\n\n" 357 | ) 358 | 359 | for i, token in enumerate(data[:10], 1): 360 | logging.info(f"Token data: {token}") 361 | current_mc_formatted = token.get('current_mc_formatted', 'N/A') 362 | ath_mc_formatted = token.get('ath_mc_formatted', 'N/A') 363 | ath_date = token.get('ath_date', 'N/A') 364 | 365 | response += ( 366 | f"{i}. {token.get('name', 'Unknown')} ({token.get('symbol', 'N/A')})\n" 367 | f" 📑 Contract Address: {token.get('address', 'N/A')}\n" 368 | f" 🗓 Date of Deployment: {token.get('deployment_date', 'N/A')}\n" 369 | f" 💥 Market Cap: {current_mc_formatted}\n" 370 | f" 🔥 ATH Market Cap: {ath_mc_formatted}\n" 371 | f" 📅 ATH Date: {ath_date}\n\n" 372 | ) 373 | 374 | # Sort by ath_market_cap in descending order and take top 3 375 | notable_tokens = sorted(data, key=lambda x: x.get('ath_market_cap', 0), reverse=True)[:2] 376 | 377 | if notable_tokens: 378 | response += f"🏆 Notable Token Deployments:\n\n" 379 | 380 | for i, token in enumerate(notable_tokens, 1): 381 | # Format market caps with appropriate suffixes 382 | starting_mc_formatted = token.get('starting_mc_formatted', '0') 383 | current_mc_formatted = token.get('current_mc_formatted', 'N/A') 384 | ath_mc_formatted = token.get('ath_mc_formatted', 'N/A') 385 | x_current_formatted = token.get('x_current_formatted', 'N/A') 386 | 387 | response += ( 388 | f"{i}. {token.get('name', 'Unknown')} ({token.get('symbol', 'N/A')})\n" 389 | f" 📑 Contract Address: {token.get('address', 'N/A')}\n" 390 | f" 🗓 Date of Deployment: {token.get('deployment_date', 'N/A')}\n" 391 | f" 🔰 Starting Market Cap: {starting_mc_formatted}\n" 392 | f" 💥 Current Market Cap: {current_mc_formatted}\n" 393 | f" 🔥 ATH Market Cap: {ath_mc_formatted}\n" 394 | f" 🤑 X's Made: {x_current_formatted}\n\n" 395 | ) 396 | 397 | keyboard = [[ 398 | InlineKeyboardButton("Track Wallet", callback_data=f"track_wd_{wallet_address}"), 399 | InlineKeyboardButton("Holding Duration", callback_data=f"wh_duration_{wallet_address}") 400 | ]] 401 | 402 | return response, keyboard 403 | 404 | async def prompt_wallet_chain_selection(update: Update, context: ContextTypes.DEFAULT_TYPE, feature: str) -> None: 405 | """ 406 | Generic function to prompt user to select a blockchain network for wallet analysis 407 | 408 | Args: 409 | update: The update object 410 | context: The context object 411 | feature: The feature identifier (e.g., 'wallet_holding_duration', etc.) 412 | """ 413 | query = update.callback_query 414 | 415 | # Create feature-specific callback data 416 | callback_prefix = f"{feature}_chain_" 417 | 418 | # Create keyboard with chain options 419 | keyboard = [ 420 | [ 421 | InlineKeyboardButton("Ethereum", callback_data=f"{callback_prefix}eth"), 422 | InlineKeyboardButton("Base", callback_data=f"{callback_prefix}base"), 423 | InlineKeyboardButton("BSC", callback_data=f"{callback_prefix}bsc") 424 | ], 425 | [InlineKeyboardButton("🔙 Back", callback_data="wallet_analysis")] 426 | ] 427 | reply_markup = InlineKeyboardMarkup(keyboard) 428 | 429 | # Store the feature in context for later use 430 | context.user_data["current_feature"] = feature 431 | 432 | # Show chain selection message 433 | await query.edit_message_text( 434 | "🔗 Select Blockchain Network\n\n" 435 | "Please choose the blockchain network for wallet analysis:", 436 | reply_markup=reply_markup, 437 | parse_mode=ParseMode.HTML 438 | ) 439 | 440 | async def handle_wallet_holding_duration_input(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 441 | """Handle wallet holding duration input""" 442 | from database import get_wallet_holding_duration 443 | 444 | await handle_wallet_analysis_input( 445 | update=update, 446 | context=context, 447 | analysis_type="wallet_holding_duration", 448 | get_data_func=get_wallet_holding_duration, 449 | format_response_func=format_wallet_holding_duration_response, 450 | scan_count_type="wallet_scan", 451 | processing_message_text="🔍 Analyzing wallet holding duration... This may take a moment.", 452 | error_message_text="❌ An error occurred while analyzing the wallet. Please try again later.", 453 | no_data_message_text="❌ Could not find holding duration data for this wallet." 454 | ) 455 | 456 | async def handle_tokens_deployed_wallet_address_input(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: 457 | """Handle tokens deployed by wallet input""" 458 | from database import get_tokens_deployed_by_wallet 459 | 460 | await handle_wallet_analysis_input( 461 | update=update, 462 | context=context, 463 | analysis_type="tokens_deployed_by_wallet", 464 | get_data_func=get_tokens_deployed_by_wallet, 465 | format_response_func=format_tokens_deployed_response, 466 | scan_count_type="wallet_scan", 467 | processing_message_text="🔍 Finding tokens deployed by this wallet... This may take a moment.", 468 | error_message_text="❌ An error occurred while analyzing the wallet. Please try again later.", 469 | no_data_message_text="❌ Could not find any tokens deployed by this wallet." 470 | ) 471 | 472 | async def handle_period_selection( 473 | update: Update, 474 | context: ContextTypes.DEFAULT_TYPE, 475 | feature_info:str, 476 | scan_type: str, 477 | callback_prefix: str 478 | ) -> None: 479 | """ 480 | Generic handler for period selection 481 | 482 | Args: 483 | update: The update object 484 | context: The context object 485 | feature_info: Description of the feature 486 | scan_type: Name of the feature for rate limiting 487 | callback_prefix: Prefix for callback data 488 | """ 489 | query = update.callback_query 490 | user = await check_callback_user(update) 491 | 492 | # Check if user has reached daily limit 493 | has_reached_limit, current_count = await check_rate_limit( 494 | user.user_id, scan_type, FREE_WALLET_SCANS_DAILY 495 | ) 496 | 497 | if has_reached_limit and not user.is_premium: 498 | keyboard = [ 499 | [InlineKeyboardButton("💎 Upgrade to Premium", callback_data="premium_info")], 500 | [InlineKeyboardButton("🔙 Back", callback_data="wallet_analysis")] 501 | ] 502 | reply_markup = InlineKeyboardMarkup(keyboard) 503 | 504 | await query.message.reply_text( 505 | f"⚠️ Daily Limit Reached\n\n" 506 | f"You've already used {current_count} out of your {FREE_WALLET_SCANS_DAILY} free daily wallet scans available for today. 🚫\n\n" 507 | f"To unlock unlimited access to powerful wallet analysis features, upgrade to Premium and explore the full potential of on-chain intelligence. 💎\n", 508 | reply_markup=reply_markup, 509 | parse_mode=ParseMode.HTML 510 | ) 511 | return 512 | 513 | # If user has not reached limit or is premium, show time period options 514 | keyboard = [ 515 | [ 516 | InlineKeyboardButton("1 Day", callback_data=f"{callback_prefix}_1"), 517 | InlineKeyboardButton("7 Days", callback_data=f"{callback_prefix}_7"), 518 | InlineKeyboardButton("30 Days", callback_data=f"{callback_prefix}_30") 519 | ], 520 | [InlineKeyboardButton("🔙 Back", callback_data="wallet_analysis")] 521 | ] 522 | reply_markup = InlineKeyboardMarkup(keyboard) 523 | 524 | # Get the selected chain 525 | selected_chain = context.user_data.get("selected_chain", "eth") 526 | 527 | await query.message.reply_text( 528 | f"🔍 Analyzing {feature_info} on {selected_chain}\n\n" 529 | f"To proceed with a more in-depth analysis, please choose the time period you'd like to examine. " 530 | f"This will help us provide insights that are both accurate and relevant to your needs.", 531 | reply_markup=reply_markup, 532 | parse_mode=ParseMode.HTML 533 | ) 534 | 535 | def format_kol_wallet_profitability_response(data: list) -> tuple: 536 | """ 537 | Format KOL wallet profitability response 538 | 539 | Args: 540 | data: List of KOL wallet profitability data 541 | 542 | Returns: 543 | Tuple of (formatted response text, keyboard buttons) 544 | """ 545 | # Create keyboard for back button 546 | keyboard = [[InlineKeyboardButton("🔙 Back", callback_data="kol_wallets")]] 547 | 548 | try: 549 | # Handle empty data case 550 | if not data: 551 | response = ( 552 | "❌ No KOL Wallet Data Found\n\n" 553 | "No KOL wallet profitability data is available for the selected period. " 554 | "This could be due to:\n" 555 | "• No trading activity in this period\n" 556 | "• API data not yet available\n" 557 | "• Temporary service issue\n\n" 558 | "Please try a different time period or check back later." 559 | ) 560 | return response, keyboard 561 | 562 | # Get period from first wallet (all should have same period) 563 | period = data[0].get("period", 30) 564 | 565 | # Determine chain from first wallet 566 | chain = data[0].get("chain", "eth") 567 | chain_display = { 568 | "eth": "Ethereum", 569 | "base": "Base", 570 | "bsc": "BSC" 571 | }.get(chain, chain.upper()) 572 | 573 | response = ( 574 | f"👑 KOL Wallets Profitability Analysis - {period} Day Overview on {chain_display}\n\n" 575 | f"🧬 Total KOL Wallets Analyzed: {len(data)} influential wallets were included in this report, " 576 | f"offering insights into how the most impactful traders have been performing during the selected period.\n\n" 577 | ) 578 | 579 | for i, wallet in enumerate(data, 1): 580 | # Safely get wallet properties with defaults 581 | address = wallet.get("address", "Unknown") 582 | name = wallet.get("name", "Unknown KOL") 583 | twitter = wallet.get("twitter", "") 584 | ens = wallet.get("ens", "") 585 | 586 | # Format wallet name with Twitter/ENS if available 587 | display_name = name 588 | if twitter: 589 | display_name += f" (@{twitter})" 590 | elif ens: 591 | display_name += f" ({ens})" 592 | 593 | # Format profit with proper currency symbol and formatting 594 | profit = wallet.get("profit", 0) 595 | profit_display = f"${profit:,.2f}" if isinstance(profit, (int, float)) else "N/A" 596 | 597 | # Format win rate with proper percentage 598 | win_rate = wallet.get("win_rate", 0) 599 | # Check if win_rate is already a percentage (0-100) or a decimal (0-1) 600 | if isinstance(win_rate, (int, float)): 601 | win_rate_display = f"{win_rate:.1f}%" if win_rate > 1 else f"{win_rate * 100:.1f}%" 602 | else: 603 | win_rate_display = "N/A" 604 | 605 | # Safely format wallet address 606 | if address and isinstance(address, str) and len(address) >= 10: 607 | address_display = f"{address}" 608 | else: 609 | address_display = "N/A" 610 | 611 | response += ( 612 | f"{i}. 🧑‍💼 {display_name}\n\n" \ 613 | f" 👜 Wallet: {address_display}\n\n" \ 614 | f" 🏆 Win Rate: {win_rate_display}\n\n" \ 615 | f" 💰 {period}-Day Profit: {profit_display}\n\n" 616 | ) 617 | 618 | # Add additional metrics if available 619 | if wallet.get("transactions"): 620 | response += f" 🔄 Transactions: {wallet.get('transactions')}\n\n" 621 | 622 | if wallet.get("tokens_traded"): 623 | response += f" 🪙 Tokens Traded: {wallet.get('tokens_traded')}\n\n" 624 | 625 | response += "\n\n" 626 | 627 | return response, keyboard 628 | 629 | except Exception as e: 630 | import logging 631 | logging.error(f"Error formatting KOL wallet profitability response: {e}", exc_info=True) 632 | 633 | # Return a fallback response in case of any error 634 | response = ( 635 | "❌ Error Formatting KOL Wallet Data\n\n" 636 | "An error occurred while formatting the KOL wallet profitability data. " 637 | "Please try again later or contact support if the issue persists." 638 | ) 639 | return response, keyboard 640 | --------------------------------------------------------------------------------