repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources15d ago| #1 | """MAWDBot Telegram Bot - Full-featured trading bot for Telegram""" |
| #2 | |
| #3 | import os |
| #4 | import asyncio |
| #5 | import logging |
| #6 | from typing import Optional |
| #7 | from pathlib import Path |
| #8 | from dotenv import load_dotenv |
| #9 | |
| #10 | # Load environment variables |
| #11 | for path in [".env.local", ".env", "../.env.local", "../.env"]: |
| #12 | if Path(path).exists(): |
| #13 | load_dotenv(path) |
| #14 | break |
| #15 | |
| #16 | # Telegram imports |
| #17 | try: |
| #18 | from telegram import Update, InlineKeyboardButton, InlineKeyboardMarkup |
| #19 | from telegram.ext import ( |
| #20 | Application, |
| #21 | CommandHandler, |
| #22 | MessageHandler, |
| #23 | CallbackQueryHandler, |
| #24 | ContextTypes, |
| #25 | filters, |
| #26 | ) |
| #27 | except ImportError: |
| #28 | print("Please install python-telegram-bot: pip install python-telegram-bot") |
| #29 | exit(1) |
| #30 | |
| #31 | from config import load_config, SolanaAgentConfig |
| #32 | from tools.solana_tools import ( |
| #33 | set_clients, create_all_tools, ToolResult, |
| #34 | GetWalletBalanceTool, GetTokenPriceTool, GetPortfolioTool, |
| #35 | GetTrendingTokensTool, SearchTokenTool, BuyTokenTool, SellTokenTool, |
| #36 | GetSwapQuoteTool, AnalyzeTokenSecurityTool, AnalyzeSolanaAddressTool, |
| #37 | GetCryptoPriceTool, GetCoinMarketDataTool, GetTrendingCryptosTool, |
| #38 | SearchCryptoTool, GetGlobalCryptoStatsTool, |
| #39 | CDPCreateAccountTool, CDPListAccountsTool, CDPGetBalanceTool, |
| #40 | AsterOpenLongTool, AsterOpenShortTool, AsterGetPositionsTool, |
| #41 | AsterClosePerpPositionTool, AsterSetLeverageTool, |
| #42 | HyperliquidOpenLongTool, HyperliquidOpenShortTool, |
| #43 | HyperliquidGetPositionsTool, HyperliquidClosePositionTool, |
| #44 | HyperliquidGetAccountTool, |
| #45 | ) |
| #46 | |
| #47 | # Logging |
| #48 | logging.basicConfig( |
| #49 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", |
| #50 | level=logging.INFO |
| #51 | ) |
| #52 | logger = logging.getLogger(__name__) |
| #53 | |
| #54 | |
| #55 | class MAWDTelegramBot: |
| #56 | """MAWDBot Telegram Interface""" |
| #57 | |
| #58 | def __init__(self): |
| #59 | self.config: Optional[SolanaAgentConfig] = None |
| #60 | self.tools = {} |
| #61 | self.initialized = False |
| #62 | self.authorized_users = set() # Add user IDs to restrict access |
| #63 | |
| #64 | async def initialize(self): |
| #65 | """Initialize the bot with all clients and tools.""" |
| #66 | logger.info("Initializing MAWDBot Telegram...") |
| #67 | |
| #68 | # Load configuration |
| #69 | self.config = load_config() |
| #70 | |
| #71 | # Import and initialize clients |
| #72 | from clients.jupiter_client import JupiterClient |
| #73 | from clients.helius_client import HeliusClient |
| #74 | from clients.birdeye_client import BirdeyeClient |
| #75 | from solders.keypair import Keypair |
| #76 | |
| #77 | jupiter_client = None |
| #78 | helius_client = None |
| #79 | birdeye_client = None |
| #80 | cdp_client = None |
| #81 | coingecko_client = None |
| #82 | aster_client = None |
| #83 | hyperliquid_client = None |
| #84 | |
| #85 | # Initialize Jupiter client |
| #86 | if self.config.jupiter_api_key and self.config.private_key: |
| #87 | try: |
| #88 | keypair = Keypair.from_base58_string(self.config.private_key) |
| #89 | jupiter_client = JupiterClient( |
| #90 | api_key=self.config.jupiter_api_key, |
| #91 | wallet_pubkey=self.config.wallet_address, |
| #92 | keypair=keypair, |
| #93 | ) |
| #94 | # await jupiter_client.initialize() # JupiterClient does not have an initialize method |
| #95 | logger.info("Jupiter client initialized") |
| #96 | except Exception as e: |
| #97 | logger.error(f"Failed to initialize JupiterClient: {e}") |
| #98 | jupiter_client = None |
| #99 | elif self.config.jupiter_api_key: |
| #100 | logger.warning("Jupiter API key present but private key is missing. JupiterClient will be disabled") |
| #101 | |
| #102 | # Initialize Helius client |
| #103 | helius_client = HeliusClient( |
| #104 | api_key=self.config.helius_api_key, |
| #105 | rpc_url=self.config.helius_rpc_url, |
| #106 | ) |
| #107 | logger.info("Helius client initialized") |
| #108 | |
| #109 | # Initialize Birdeye client |
| #110 | birdeye_client = BirdeyeClient(api_key=self.config.birdeye_api_key) |
| #111 | logger.info("Birdeye client initialized") |
| #112 | |
| #113 | # Initialize Bags client |
| #114 | try: |
| #115 | from clients.bags_client import BagsClient |
| #116 | bags_client = BagsClient( |
| #117 | api_key=self.config.bags_api_key, |
| #118 | config_key=self.config.bags_config_key, |
| #119 | rpc_url=self.config.helius_rpc_url, |
| #120 | private_key=self.config.private_key, |
| #121 | ) |
| #122 | logger.info("Bags client initialized") |
| #123 | except Exception as e: |
| #124 | logger.warning(f"Bags client not available: {e}") |
| #125 | bags_client = None |
| #126 | |
| #127 | # Initialize CDP client (optional) |
| #128 | if self.config.cdp_api_key_id and self.config.cdp_api_key_secret: |
| #129 | try: |
| #130 | from clients.cdp_client import create_cdp_client |
| #131 | cdp_client = create_cdp_client( |
| #132 | api_key_id=self.config.cdp_api_key_id, |
| #133 | api_key_secret=self.config.cdp_api_key_secret, |
| #134 | wallet_secret=self.config.cdp_wallet_secret, |
| #135 | rpc_url=self.config.cdp_rpc_url, |
| #136 | network=self.config.cdp_network, |
| #137 | ) |
| #138 | if cdp_client: |
| #139 | logger.info("CDP client initialized") |
| #140 | except Exception as e: |
| #141 | logger.warning(f"CDP client not available: {e}") |
| #142 | cdp_client = None |
| #143 | else: |
| #144 | cdp_client = None |
| #145 | |
| #146 | # Initialize CoinGecko client (optional) |
| #147 | if self.config.coingecko_api_key: |
| #148 | try: |
| #149 | from clients.coingecko_client import create_coingecko_client |
| #150 | coingecko_client = create_coingecko_client(api_key=self.config.coingecko_api_key) |
| #151 | logger.info("CoinGecko client initialized") |
| #152 | except Exception as e: |
| #153 | logger.warning(f"CoinGecko client not available: {e}") |
| #154 | |
| #155 | # Initialize Aster client (optional) |
| #156 | if self.config.aster_user_address and self.config.aster_signer_address and self.config.aster_private_key: |
| #157 | try: |
| #158 | from clients.aster_client import AsterClient |
| #159 | aster_client = AsterClient( |
| #160 | user_address=self.config.aster_user_address, |
| #161 | signer_address=self.config.aster_signer_address, |
| #162 | private_key=self.config.aster_private_key, |
| #163 | ) |
| #164 | logger.info("Aster client initialized") |
| #165 | except Exception as e: |
| #166 | logger.warning(f"Aster client not available: {e}") |
| #167 | |
| #168 | # Initialize Hyperliquid client (optional) |
| #169 | if self.config.hyperliquid_wallet and self.config.hyperliquid_private_key: |
| #170 | try: |
| #171 | from clients.hyperliquid_client import HyperliquidClient |
| #172 | hyperliquid_client = HyperliquidClient( |
| #173 | wallet_address=self.config.hyperliquid_wallet, |
| #174 | private_key=self.config.hyperliquid_private_key, |
| #175 | use_testnet=self.config.hyperliquid_use_testnet, |
| #176 | ) |
| #177 | logger.info("Hyperliquid client initialized") |
| #178 | except Exception as e: |
| #179 | logger.warning(f"Hyperliquid client not available: {e}") |
| #180 | |
| #181 | # Initialize PumpFun client (for pump.fun token launches) |
| #182 | pumpfun_client = None |
| #183 | if self.config.private_key and self.config.helius_rpc_url: |
| #184 | try: |
| #185 | from clients.pumpfun_client import PumpFunClient |
| #186 | pumpfun_client = PumpFunClient( |
| #187 | rpc_url=self.config.helius_rpc_url, |
| #188 | private_key=self.config.private_key, |
| #189 | ) |
| #190 | logger.info("PumpFun client initialized") |
| #191 | except Exception as e: |
| #192 | logger.warning(f"PumpFun client not available: {e}") |
| #193 | |
| #194 | # Set clients for tools |
| #195 | set_clients( |
| #196 | jupiter_client=jupiter_client, |
| #197 | helius_client=helius_client, |
| #198 | birdeye_client=birdeye_client, |
| #199 | cdp_client=cdp_client, |
| #200 | coingecko_client=coingecko_client, |
| #201 | aster_client=aster_client, |
| #202 | hyperliquid_client=hyperliquid_client, |
| #203 | bags_client=bags_client, |
| #204 | pumpfun_client=pumpfun_client, |
| #205 | ) |
| #206 | |
| #207 | # Create tool instances |
| #208 | self.tools = {tool.name: tool for tool in create_all_tools()} |
| #209 | logger.info(f"Loaded {len(self.tools)} tools") |
| #210 | |
| #211 | self.initialized = True |
| #212 | logger.info("MAWDBot Telegram initialized successfully!") |
| #213 | |
| #214 | # ============================================================ |
| #215 | # COMMAND HANDLERS |
| #216 | # ============================================================ |
| #217 | |
| #218 | async def start(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #219 | """Handle /start command.""" |
| #220 | welcome_message = """ |
| #221 | **Welcome to MAWDBot!** |
| #222 | |
| #223 | I'm your AI-powered Solana trading assistant. I can help you: |
| #224 | |
| #225 | **Trading:** |
| #226 | - Check wallet balances and portfolio |
| #227 | - Buy/sell tokens on Solana |
| #228 | - Trade perpetuals on Aster & Hyperliquid |
| #229 | |
| #230 | **Market Data:** |
| #231 | - Get real-time crypto prices |
| #232 | - Find trending tokens |
| #233 | - Analyze token security |
| #234 | |
| #235 | **CDP Wallets:** |
| #236 | - Create secure custodial accounts |
| #237 | - Manage CDP wallets |
| #238 | |
| #239 | Type /help to see all available commands. |
| #240 | |
| #241 | **Exfoliate, trade, launch, vibe!** |
| #242 | """ |
| #243 | await update.message.reply_text(welcome_message, parse_mode="Markdown") |
| #244 | |
| #245 | async def help_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #246 | """Handle /help command.""" |
| #247 | help_text = """ |
| #248 | **MAWDBot Commands** |
| #249 | |
| #250 | **Wallet & Portfolio:** |
| #251 | /balance - Check SOL and token balances |
| #252 | /portfolio - View complete portfolio |
| #253 | /networth - Detailed net worth breakdown |
| #254 | |
| #255 | **Trading:** |
| #256 | /price <token> - Get token price |
| #257 | /buy <amount> <token> - Buy tokens |
| #258 | /sell <amount> <token> - Sell tokens |
| #259 | /quote <amount> <from> <to> - Get swap quote |
| #260 | |
| #261 | **Market Data:** |
| #262 | /trending - Trending Solana tokens |
| #263 | /search <query> - Search for tokens |
| #264 | /crypto <coins> - CoinGecko prices |
| #265 | /global - Global crypto stats |
| #266 | /trendingcrypto - Trending on CoinGecko |
| #267 | |
| #268 | **Perpetuals (Aster/Hyperliquid):** |
| #269 | /long <pair> <leverage> <amount> - Open LONG |
| #270 | /short <pair> <leverage> <amount> - Open SHORT |
| #271 | /positions - View open positions |
| #272 | /close <pair> - Close position |
| #273 | |
| #274 | **CDP Wallets:** |
| #275 | /cdp_create - Create CDP wallet |
| #276 | /cdp_list - List CDP wallets |
| #277 | /cdp_balance <address> - Check balance |
| #278 | |
| #279 | **Analysis:** |
| #280 | /analyze <address> - Analyze Solana address |
| #281 | /security <token> - Token security check |
| #282 | |
| #283 | **Other:** |
| #284 | /chat <message> - Chat with AI |
| #285 | /help - Show this help |
| #286 | """ |
| #287 | await update.message.reply_text(help_text) |
| #288 | |
| #289 | async def balance(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #290 | """Handle /balance command.""" |
| #291 | await update.message.reply_text("Checking wallet balance...") |
| #292 | |
| #293 | tool = GetWalletBalanceTool() |
| #294 | result = await tool.execute() |
| #295 | |
| #296 | if result.success: |
| #297 | await update.message.reply_text(f"```\n{result.content}\n```", parse_mode="Markdown") |
| #298 | else: |
| #299 | await update.message.reply_text(f"Error: {result.error}") |
| #300 | |
| #301 | async def portfolio(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #302 | """Handle /portfolio command.""" |
| #303 | await update.message.reply_text("Loading portfolio...") |
| #304 | |
| #305 | tool = GetPortfolioTool() |
| #306 | result = await tool.execute() |
| #307 | |
| #308 | if result.success: |
| #309 | await update.message.reply_text(f"```\n{result.content}\n```", parse_mode="Markdown") |
| #310 | else: |
| #311 | await update.message.reply_text(f"Error: {result.error}") |
| #312 | |
| #313 | async def price(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #314 | """Handle /price <token> command.""" |
| #315 | if not context.args: |
| #316 | await update.message.reply_text("Usage: /price <token>\nExample: /price BONK") |
| #317 | return |
| #318 | |
| #319 | token = " ".join(context.args) |
| #320 | await update.message.reply_text(f"Getting price for {token}...") |
| #321 | |
| #322 | tool = GetTokenPriceTool() |
| #323 | result = await tool.execute(token_address=token) |
| #324 | |
| #325 | if result.success: |
| #326 | await update.message.reply_text(result.content) |
| #327 | else: |
| #328 | await update.message.reply_text(f"Error: {result.error}") |
| #329 | |
| #330 | async def trending(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #331 | """Handle /trending command.""" |
| #332 | await update.message.reply_text("Fetching trending Solana tokens...") |
| #333 | |
| #334 | tool = GetTrendingTokensTool() |
| #335 | result = await tool.execute() |
| #336 | |
| #337 | if result.success: |
| #338 | # Truncate if too long for Telegram |
| #339 | content = result.content[:4000] if len(result.content) > 4000 else result.content |
| #340 | await update.message.reply_text(f"```\n{content}\n```", parse_mode="Markdown") |
| #341 | else: |
| #342 | await update.message.reply_text(f"Error: {result.error}") |
| #343 | |
| #344 | async def search(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #345 | """Handle /search <query> command.""" |
| #346 | if not context.args: |
| #347 | await update.message.reply_text("Usage: /search <query>\nExample: /search BONK") |
| #348 | return |
| #349 | |
| #350 | query = " ".join(context.args) |
| #351 | await update.message.reply_text(f"Searching for '{query}'...") |
| #352 | |
| #353 | tool = SearchTokenTool() |
| #354 | result = await tool.execute(query=query) |
| #355 | |
| #356 | if result.success: |
| #357 | content = result.content[:4000] if len(result.content) > 4000 else result.content |
| #358 | await update.message.reply_text(f"```\n{content}\n```", parse_mode="Markdown") |
| #359 | else: |
| #360 | await update.message.reply_text(f"Error: {result.error}") |
| #361 | |
| #362 | async def buy(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #363 | """Handle /buy <amount> <token> command.""" |
| #364 | if len(context.args) < 2: |
| #365 | await update.message.reply_text("Usage: /buy <amount_sol> <token>\nExample: /buy 0.5 BONK") |
| #366 | return |
| #367 | |
| #368 | try: |
| #369 | amount = float(context.args[0]) |
| #370 | token = " ".join(context.args[1:]) |
| #371 | except ValueError: |
| #372 | await update.message.reply_text("Invalid amount. Usage: /buy <amount_sol> <token>") |
| #373 | return |
| #374 | |
| #375 | # Send confirmation message with inline keyboard |
| #376 | keyboard = [ |
| #377 | [ |
| #378 | InlineKeyboardButton("Confirm", callback_data=f"buy_confirm_{amount}_{token}"), |
| #379 | InlineKeyboardButton("Cancel", callback_data="buy_cancel"), |
| #380 | ] |
| #381 | ] |
| #382 | reply_markup = InlineKeyboardMarkup(keyboard) |
| #383 | |
| #384 | await update.message.reply_text( |
| #385 | f"**Buy Confirmation**\n\n" |
| #386 | f"Amount: {amount} SOL\n" |
| #387 | f"Token: {token}\n\n" |
| #388 | f"Are you sure you want to proceed?", |
| #389 | reply_markup=reply_markup, |
| #390 | parse_mode="Markdown" |
| #391 | ) |
| #392 | |
| #393 | async def sell(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #394 | """Handle /sell <amount> <token> command.""" |
| #395 | if len(context.args) < 2: |
| #396 | await update.message.reply_text("Usage: /sell <amount> <token>\nExample: /sell 1000000 BONK") |
| #397 | return |
| #398 | |
| #399 | try: |
| #400 | amount = float(context.args[0]) |
| #401 | token = " ".join(context.args[1:]) |
| #402 | except ValueError: |
| #403 | await update.message.reply_text("Invalid amount. Usage: /sell <amount> <token>") |
| #404 | return |
| #405 | |
| #406 | keyboard = [ |
| #407 | [ |
| #408 | InlineKeyboardButton("Confirm", callback_data=f"sell_confirm_{amount}_{token}"), |
| #409 | InlineKeyboardButton("Cancel", callback_data="sell_cancel"), |
| #410 | ] |
| #411 | ] |
| #412 | reply_markup = InlineKeyboardMarkup(keyboard) |
| #413 | |
| #414 | await update.message.reply_text( |
| #415 | f"**Sell Confirmation**\n\n" |
| #416 | f"Amount: {amount}\n" |
| #417 | f"Token: {token}\n\n" |
| #418 | f"Are you sure you want to proceed?", |
| #419 | reply_markup=reply_markup, |
| #420 | parse_mode="Markdown" |
| #421 | ) |
| #422 | |
| #423 | async def quote(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #424 | """Handle /quote <amount> <from> <to> command.""" |
| #425 | if len(context.args) < 3: |
| #426 | await update.message.reply_text("Usage: /quote <amount> <from_token> <to_token>\nExample: /quote 1 SOL USDC") |
| #427 | return |
| #428 | |
| #429 | try: |
| #430 | amount = float(context.args[0]) |
| #431 | from_token = context.args[1] |
| #432 | to_token = context.args[2] |
| #433 | except ValueError: |
| #434 | await update.message.reply_text("Invalid amount.") |
| #435 | return |
| #436 | |
| #437 | await update.message.reply_text(f"Getting quote for {amount} {from_token} -> {to_token}...") |
| #438 | |
| #439 | tool = GetSwapQuoteTool() |
| #440 | result = await tool.execute( |
| #441 | input_token=from_token, |
| #442 | output_token=to_token, |
| #443 | amount=amount, |
| #444 | ) |
| #445 | |
| #446 | if result.success: |
| #447 | await update.message.reply_text(f"```\n{result.content}\n```", parse_mode="Markdown") |
| #448 | else: |
| #449 | await update.message.reply_text(f"Error: {result.error}") |
| #450 | |
| #451 | async def crypto(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #452 | """Handle /crypto <coins> command for CoinGecko prices.""" |
| #453 | if not context.args: |
| #454 | await update.message.reply_text("Usage: /crypto <coins>\nExample: /crypto bitcoin,ethereum,solana") |
| #455 | return |
| #456 | |
| #457 | coins = context.args[0].split(",") |
| #458 | await update.message.reply_text(f"Getting prices for {', '.join(coins)}...") |
| #459 | |
| #460 | tool = GetCryptoPriceTool() |
| #461 | result = await tool.execute(coin_ids=coins) |
| #462 | |
| #463 | if result.success: |
| #464 | await update.message.reply_text(result.content) |
| #465 | else: |
| #466 | await update.message.reply_text(f"Error: {result.error}") |
| #467 | |
| #468 | async def global_stats(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #469 | """Handle /global command for global crypto stats.""" |
| #470 | await update.message.reply_text("Getting global crypto market stats...") |
| #471 | |
| #472 | tool = GetGlobalCryptoStatsTool() |
| #473 | result = await tool.execute() |
| #474 | |
| #475 | if result.success: |
| #476 | await update.message.reply_text(result.content) |
| #477 | else: |
| #478 | await update.message.reply_text(f"Error: {result.error}") |
| #479 | |
| #480 | async def trending_crypto(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #481 | """Handle /trendingcrypto command.""" |
| #482 | await update.message.reply_text("Getting trending cryptocurrencies...") |
| #483 | |
| #484 | tool = GetTrendingCryptosTool() |
| #485 | result = await tool.execute() |
| #486 | |
| #487 | if result.success: |
| #488 | content = result.content[:4000] if len(result.content) > 4000 else result.content |
| #489 | await update.message.reply_text(content) |
| #490 | else: |
| #491 | await update.message.reply_text(f"Error: {result.error}") |
| #492 | |
| #493 | async def analyze(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #494 | """Handle /analyze <address> command.""" |
| #495 | if not context.args: |
| #496 | await update.message.reply_text("Usage: /analyze <solana_address>\nExample: /analyze DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263") |
| #497 | return |
| #498 | |
| #499 | address = context.args[0] |
| #500 | await update.message.reply_text(f"Analyzing {address}...") |
| #501 | |
| #502 | tool = AnalyzeSolanaAddressTool() |
| #503 | result = await tool.execute(address=address) |
| #504 | |
| #505 | if result.success: |
| #506 | content = result.content[:4000] if len(result.content) > 4000 else result.content |
| #507 | await update.message.reply_text(f"```\n{content}\n```", parse_mode="Markdown") |
| #508 | else: |
| #509 | await update.message.reply_text(f"Error: {result.error}") |
| #510 | |
| #511 | async def security(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #512 | """Handle /security <token> command.""" |
| #513 | if not context.args: |
| #514 | await update.message.reply_text("Usage: /security <token_address>\nExample: /security DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263") |
| #515 | return |
| #516 | |
| #517 | token = context.args[0] |
| #518 | await update.message.reply_text(f"Analyzing token security for {token}...") |
| #519 | |
| #520 | tool = AnalyzeTokenSecurityTool() |
| #521 | result = await tool.execute(token_address=token) |
| #522 | |
| #523 | if result.success: |
| #524 | content = result.content[:4000] if len(result.content) > 4000 else result.content |
| #525 | await update.message.reply_text(f"```\n{content}\n```", parse_mode="Markdown") |
| #526 | else: |
| #527 | await update.message.reply_text(f"Error: {result.error}") |
| #528 | |
| #529 | async def cdp_create(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #530 | """Handle /cdp_create command.""" |
| #531 | await update.message.reply_text("Creating new CDP Solana account...") |
| #532 | |
| #533 | tool = CDPCreateAccountTool() |
| #534 | name = context.args[0] if context.args else None |
| #535 | result = await tool.execute(name=name) |
| #536 | |
| #537 | if result.success: |
| #538 | await update.message.reply_text(f"```\n{result.content}\n```", parse_mode="Markdown") |
| #539 | else: |
| #540 | await update.message.reply_text(f"Error: {result.error}") |
| #541 | |
| #542 | async def cdp_list(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #543 | """Handle /cdp_list command.""" |
| #544 | await update.message.reply_text("Listing CDP accounts...") |
| #545 | |
| #546 | tool = CDPListAccountsTool() |
| #547 | result = await tool.execute() |
| #548 | |
| #549 | if result.success: |
| #550 | await update.message.reply_text(f"```\n{result.content}\n```", parse_mode="Markdown") |
| #551 | else: |
| #552 | await update.message.reply_text(f"Error: {result.error}") |
| #553 | |
| #554 | async def cdp_balance(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #555 | """Handle /cdp_balance <address> command.""" |
| #556 | if not context.args: |
| #557 | await update.message.reply_text("Usage: /cdp_balance <address>") |
| #558 | return |
| #559 | |
| #560 | address = context.args[0] |
| #561 | await update.message.reply_text(f"Checking CDP balance for {address}...") |
| #562 | |
| #563 | tool = CDPGetBalanceTool() |
| #564 | result = await tool.execute(address=address) |
| #565 | |
| #566 | if result.success: |
| #567 | await update.message.reply_text(result.content) |
| #568 | else: |
| #569 | await update.message.reply_text(f"Error: {result.error}") |
| #570 | |
| #571 | async def positions(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #572 | """Handle /positions command.""" |
| #573 | await update.message.reply_text("Fetching open positions...") |
| #574 | |
| #575 | # Try Aster first, then Hyperliquid |
| #576 | aster_tool = AsterGetPositionsTool() |
| #577 | aster_result = await aster_tool.execute() |
| #578 | |
| #579 | hl_tool = HyperliquidGetPositionsTool() |
| #580 | hl_result = await hl_tool.execute() |
| #581 | |
| #582 | content = "" |
| #583 | if aster_result.success: |
| #584 | content += f"**Aster DEX:**\n```\n{aster_result.content}\n```\n\n" |
| #585 | if hl_result.success: |
| #586 | content += f"**Hyperliquid:**\n```\n{hl_result.content}\n```" |
| #587 | |
| #588 | if content: |
| #589 | await update.message.reply_text(content, parse_mode="Markdown") |
| #590 | else: |
| #591 | await update.message.reply_text("No positions found or error fetching positions.") |
| #592 | |
| #593 | async def long_position(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #594 | """Handle /long <pair> <leverage> <amount> command.""" |
| #595 | if len(context.args) < 3: |
| #596 | await update.message.reply_text("Usage: /long <pair> <leverage> <amount>\nExample: /long BTCUSDT 10 100") |
| #597 | return |
| #598 | |
| #599 | pair = context.args[0].upper() |
| #600 | try: |
| #601 | leverage = int(context.args[1]) |
| #602 | amount = float(context.args[2]) |
| #603 | except ValueError: |
| #604 | await update.message.reply_text("Invalid leverage or amount.") |
| #605 | return |
| #606 | |
| #607 | keyboard = [ |
| #608 | [ |
| #609 | InlineKeyboardButton("Confirm LONG", callback_data=f"long_confirm_{pair}_{leverage}_{amount}"), |
| #610 | InlineKeyboardButton("Cancel", callback_data="trade_cancel"), |
| #611 | ] |
| #612 | ] |
| #613 | reply_markup = InlineKeyboardMarkup(keyboard) |
| #614 | |
| #615 | await update.message.reply_text( |
| #616 | f"**LONG Position Confirmation**\n\n" |
| #617 | f"Pair: {pair}\n" |
| #618 | f"Leverage: {leverage}x\n" |
| #619 | f"Amount: {amount} USDT\n\n" |
| #620 | f"**Warning:** Leveraged trading is high risk!\n" |
| #621 | f"Liquidation can result in total loss of margin.\n\n" |
| #622 | f"Proceed?", |
| #623 | reply_markup=reply_markup, |
| #624 | parse_mode="Markdown" |
| #625 | ) |
| #626 | |
| #627 | async def short_position(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #628 | """Handle /short <pair> <leverage> <amount> command.""" |
| #629 | if len(context.args) < 3: |
| #630 | await update.message.reply_text("Usage: /short <pair> <leverage> <amount>\nExample: /short ETHUSDT 5 50") |
| #631 | return |
| #632 | |
| #633 | pair = context.args[0].upper() |
| #634 | try: |
| #635 | leverage = int(context.args[1]) |
| #636 | amount = float(context.args[2]) |
| #637 | except ValueError: |
| #638 | await update.message.reply_text("Invalid leverage or amount.") |
| #639 | return |
| #640 | |
| #641 | keyboard = [ |
| #642 | [ |
| #643 | InlineKeyboardButton("Confirm SHORT", callback_data=f"short_confirm_{pair}_{leverage}_{amount}"), |
| #644 | InlineKeyboardButton("Cancel", callback_data="trade_cancel"), |
| #645 | ] |
| #646 | ] |
| #647 | reply_markup = InlineKeyboardMarkup(keyboard) |
| #648 | |
| #649 | await update.message.reply_text( |
| #650 | f"**SHORT Position Confirmation**\n\n" |
| #651 | f"Pair: {pair}\n" |
| #652 | f"Leverage: {leverage}x\n" |
| #653 | f"Amount: {amount} USDT\n\n" |
| #654 | f"**Warning:** Leveraged trading is high risk!\n" |
| #655 | f"Liquidation can result in total loss of margin.\n\n" |
| #656 | f"Proceed?", |
| #657 | reply_markup=reply_markup, |
| #658 | parse_mode="Markdown" |
| #659 | ) |
| #660 | |
| #661 | async def close_position(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #662 | """Handle /close <pair> command.""" |
| #663 | if not context.args: |
| #664 | await update.message.reply_text("Usage: /close <pair>\nExample: /close BTCUSDT") |
| #665 | return |
| #666 | |
| #667 | pair = context.args[0].upper() |
| #668 | |
| #669 | keyboard = [ |
| #670 | [ |
| #671 | InlineKeyboardButton("Confirm Close", callback_data=f"close_confirm_{pair}"), |
| #672 | InlineKeyboardButton("Cancel", callback_data="trade_cancel"), |
| #673 | ] |
| #674 | ] |
| #675 | reply_markup = InlineKeyboardMarkup(keyboard) |
| #676 | |
| #677 | await update.message.reply_text( |
| #678 | f"**Close Position Confirmation**\n\n" |
| #679 | f"Pair: {pair}\n\n" |
| #680 | f"This will close your entire position.\n" |
| #681 | f"Proceed?", |
| #682 | reply_markup=reply_markup, |
| #683 | parse_mode="Markdown" |
| #684 | ) |
| #685 | |
| #686 | async def button_callback(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #687 | """Handle inline button callbacks.""" |
| #688 | query = update.callback_query |
| #689 | await query.answer() |
| #690 | |
| #691 | data = query.data |
| #692 | |
| #693 | if data == "buy_cancel" or data == "sell_cancel" or data == "trade_cancel": |
| #694 | await query.edit_message_text("Trade cancelled.") |
| #695 | return |
| #696 | |
| #697 | if data.startswith("buy_confirm_"): |
| #698 | parts = data.split("_") |
| #699 | amount = float(parts[2]) |
| #700 | token = "_".join(parts[3:]) |
| #701 | |
| #702 | await query.edit_message_text(f"Executing buy: {amount} SOL -> {token}...") |
| #703 | |
| #704 | tool = BuyTokenTool() |
| #705 | result = await tool.execute( |
| #706 | token_address=token, |
| #707 | amount_sol=amount, |
| #708 | slippage_bps=300, |
| #709 | ) |
| #710 | |
| #711 | if result.success: |
| #712 | await query.edit_message_text(f"Buy executed!\n```\n{result.content}\n```", parse_mode="Markdown") |
| #713 | else: |
| #714 | await query.edit_message_text(f"Buy failed: {result.error}") |
| #715 | |
| #716 | elif data.startswith("sell_confirm_"): |
| #717 | parts = data.split("_") |
| #718 | amount = float(parts[2]) |
| #719 | token = "_".join(parts[3:]) |
| #720 | |
| #721 | await query.edit_message_text(f"Executing sell: {amount} {token}...") |
| #722 | |
| #723 | tool = SellTokenTool() |
| #724 | result = await tool.execute( |
| #725 | token_address=token, |
| #726 | amount=amount, |
| #727 | slippage_bps=300, |
| #728 | ) |
| #729 | |
| #730 | if result.success: |
| #731 | await query.edit_message_text(f"Sell executed!\n```\n{result.content}\n```", parse_mode="Markdown") |
| #732 | else: |
| #733 | await query.edit_message_text(f"Sell failed: {result.error}") |
| #734 | |
| #735 | elif data.startswith("long_confirm_"): |
| #736 | parts = data.split("_") |
| #737 | pair = parts[2] |
| #738 | leverage = int(parts[3]) |
| #739 | amount = float(parts[4]) |
| #740 | |
| #741 | await query.edit_message_text(f"Opening LONG position on {pair}...") |
| #742 | |
| #743 | tool = AsterOpenLongTool() |
| #744 | result = await tool.execute( |
| #745 | symbol=pair, |
| #746 | leverage=leverage, |
| #747 | margin_usdt=amount, |
| #748 | ) |
| #749 | |
| #750 | if result.success: |
| #751 | await query.edit_message_text(f"LONG opened!\n```\n{result.content}\n```", parse_mode="Markdown") |
| #752 | else: |
| #753 | await query.edit_message_text(f"Failed: {result.error}") |
| #754 | |
| #755 | elif data.startswith("short_confirm_"): |
| #756 | parts = data.split("_") |
| #757 | pair = parts[2] |
| #758 | leverage = int(parts[3]) |
| #759 | amount = float(parts[4]) |
| #760 | |
| #761 | await query.edit_message_text(f"Opening SHORT position on {pair}...") |
| #762 | |
| #763 | tool = AsterOpenShortTool() |
| #764 | result = await tool.execute( |
| #765 | symbol=pair, |
| #766 | leverage=leverage, |
| #767 | margin_usdt=amount, |
| #768 | ) |
| #769 | |
| #770 | if result.success: |
| #771 | await query.edit_message_text(f"SHORT opened!\n```\n{result.content}\n```", parse_mode="Markdown") |
| #772 | else: |
| #773 | await query.edit_message_text(f"Failed: {result.error}") |
| #774 | |
| #775 | elif data.startswith("close_confirm_"): |
| #776 | pair = data.split("_")[2] |
| #777 | |
| #778 | await query.edit_message_text(f"Closing position on {pair}...") |
| #779 | |
| #780 | tool = AsterClosePerpPositionTool() |
| #781 | result = await tool.execute(symbol=pair) |
| #782 | |
| #783 | if result.success: |
| #784 | await query.edit_message_text(f"Position closed!\n```\n{result.content}\n```", parse_mode="Markdown") |
| #785 | else: |
| #786 | await query.edit_message_text(f"Failed: {result.error}") |
| #787 | |
| #788 | async def chat(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #789 | """Handle /chat <message> for AI conversation using Gemini.""" |
| #790 | if not context.args: |
| #791 | await update.message.reply_text("Usage: /chat <your message>\nExample: /chat What's the best memecoin to buy?") |
| #792 | return |
| #793 | |
| #794 | message = " ".join(context.args) |
| #795 | await update.message.reply_text(f"Gemini is thinking...") |
| #796 | |
| #797 | # Get API key |
| #798 | api_key = os.getenv("GOOGLE_API_KEY") |
| #799 | if not api_key: |
| #800 | await update.message.reply_text("Error: GOOGLE_API_KEY not set.") |
| #801 | return |
| #802 | |
| #803 | # Configure and generate |
| #804 | import google.generativeai as genai |
| #805 | import asyncio |
| #806 | |
| #807 | genai.configure(api_key=api_key) |
| #808 | model = genai.GenerativeModel('gemini-1.5-flash') |
| #809 | |
| #810 | try: |
| #811 | # Run the synchronous generate_content in a thread |
| #812 | loop = asyncio.get_event_loop() |
| #813 | response = await loop.run_in_executor( |
| #814 | None, |
| #815 | lambda: model.generate_content(message) |
| #816 | ) |
| #817 | # Get the text from the response |
| #818 | if response.text: |
| #819 | # Truncate if too long for Telegram |
| #820 | if len(response.text) > 4096: |
| #821 | response_text = response.text[:4000] + "\n\n... (truncated)" |
| #822 | else: |
| #823 | response_text = response.text |
| #824 | await update.message.reply_text(response_text) |
| #825 | else: |
| #826 | await update.message.reply_text("Gemini did not return any text.") |
| #827 | except Exception as e: |
| #828 | await update.message.reply_text(f"Gemini error: {str(e)}") |
| #829 | |
| #830 | async def browse(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #831 | """Handle /browse <instruction> command for browser automation.""" |
| #832 | if not context.args: |
| #833 | await update.message.reply_text("Usage: /browse <instruction>\nExample: /browse Search for the weather in New York") |
| #834 | return |
| #835 | |
| #836 | instruction = " ".join(context.args) |
| #837 | await update.message.reply_text(f"Starting browser automation: {instruction}") |
| #838 | |
| #839 | # Import Playwright |
| #840 | from playwright.sync_api import sync_playwright |
| #841 | import google.generativeai as genai |
| #842 | from google.generativeai.types import Part |
| #843 | import base64 |
| #844 | import io |
| #845 | |
| #846 | # Get Gemini API key |
| #847 | api_key = os.getenv("GOOGLE_API_KEY") |
| #848 | if not api_key: |
| #849 | await update.message.reply_text("Error: GOOGLE_API_KEY not set.") |
| #850 | return |
| #851 | |
| #852 | genai.configure(api_key=api_key) |
| #853 | |
| #854 | # Configure the model |
| #855 | model = genai.GenerativeModel('gemini-2.5-computer-use-preview-10-2025') |
| #856 | |
| #857 | # Start browser |
| #858 | with sync_playwright() as p: |
| #859 | browser = p.chromium.launch(headless=True) |
| #860 | page = browser.new_page() |
| #861 | page.goto("https://www.google.com") |
| #862 | page.set_viewport_size({"width": 1440, "height": 900}) |
| #863 | |
| #864 | # Main agent loop |
| #865 | for i in range(5): # Limit to 5 steps to avoid infinite loops |
| #866 | # Take screenshot |
| #867 | screenshot = page.screenshot(type="png") |
| #868 | screenshot_base64 = base64.b64encode(screenshot).decode('utf-8') |
| #869 | |
| #870 | # Create content with instruction and image |
| #871 | content = [ |
| #872 | Part.from_text(instruction), |
| #873 | Part.from_data( |
| #874 | data=base64.b64decode(screenshot_base64), |
| #875 | mime_type="image/png" |
| #876 | ) |
| #877 | ] |
| #878 | |
| #879 | # Generate content |
| #880 | try: |
| #881 | response = model.generate_content(content) |
| #882 | text_response = response.text |
| #883 | except Exception as e: |
| #884 | await update.message.reply_text(f"Gemini error: {str(e)}") |
| #885 | break |
| #886 | |
| #887 | # Check if there are function calls |
| #888 | function_calls = [] |
| #889 | for part in response.candidates[0].content.parts: |
| #890 | if hasattr(part, 'function_call'): |
| #891 | function_calls.append(part.function_call) |
| #892 | |
| #893 | if not function_calls: |
| #894 | # If no function calls, assume the task is complete |
| #895 | await update.message.reply_text(f"Task completed: {text_response}") |
| #896 | break |
| #897 | |
| #898 | # Execute function calls |
| #899 | for function_call in function_calls: |
| #900 | # For simplicity, we only handle click_at and type_text_at |
| #901 | if function_call.name == "click_at": |
| #902 | x = function_call.args['x'] |
| #903 | y = function_call.args['y'] |
| #904 | # Convert normalized coordinates to pixels |
| #905 | viewport = page.viewport_size |
| #906 | actual_x = int(x / 1000 * viewport['width']) |
| #907 | actual_y = int(y / 1000 * viewport['height']) |
| #908 | page.mouse.click(actual_x, actual_y) |
| #909 | elif function_call.name == "type_text_at": |
| #910 | x = function_call.args['x'] |
| #911 | y = function_call.args['y'] |
| #912 | text = function_call.args['text'] |
| #913 | press_enter = function_call.args.get('press_enter', False) |
| #914 | viewport = page.viewport_size |
| #915 | actual_x = int(x / 1000 * viewport['width']) |
| #916 | actual_y = int(y / 1000 * viewport['height']) |
| #917 | page.mouse.click(actual_x, actual_y) |
| #918 | page.keyboard.type(text) |
| #919 | if press_enter: |
| #920 | page.keyboard.press("Enter") |
| #921 | else: |
| #922 | logger.warning(f"Unsupported function call: {function_call.name}") |
| #923 | |
| #924 | # Wait for page to settle |
| #925 | page.wait_for_timeout(1000) |
| #926 | |
| #927 | # Close browser |
| #928 | browser.close() |
| #929 | |
| #930 | await update.message.reply_text("Browser automation completed.") |
| #931 | async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE): |
| #932 | """Handle regular text messages.""" |
| #933 | text = update.message.text.lower() |
| #934 | |
| #935 | # Quick responses for common queries |
| #936 | if "balance" in text: |
| #937 | await self.balance(update, context) |
| #938 | elif "portfolio" in text: |
| #939 | await self.portfolio(update, context) |
| #940 | elif "trending" in text: |
| #941 | await self.trending(update, context) |
| #942 | elif "help" in text: |
| #943 | await self.help_command(update, context) |
| #944 | else: |
| #945 | await update.message.reply_text( |
| #946 | "I didn't understand that. Use /help to see available commands, " |
| #947 | "or use /chat <message> to ask me anything!" |
| #948 | ) |
| #949 | |
| #950 | |
| #951 | def main(): |
| #952 | """Main entry point.""" |
| #953 | # Get bot token from environment |
| #954 | bot_token = os.getenv("TELEGRAM_BOT_TOKEN") |
| #955 | |
| #956 | if not bot_token: |
| #957 | print("Error: TELEGRAM_BOT_TOKEN not found in environment variables.") |
| #958 | print("Add TELEGRAM_BOT_TOKEN=<redacted> to your .env.local file") |
| #959 | print("\nTo get a bot token:") |
| #960 | print("1. Message @BotFather on Telegram") |
| #961 | print("2. Send /newbot and follow instructions") |
| #962 | print("3. Copy the token to your .env.local file") |
| #963 | return |
| #964 | |
| #965 | # Create bot instance |
| #966 | bot = MAWDTelegramBot() |
| #967 | |
| #968 | # Create application |
| #969 | application = Application.builder().token(bot_token).build() |
| #970 | |
| #971 | # Initialize bot clients on startup |
| #972 | async def post_init(app): |
| #973 | await bot.initialize() |
| #974 | |
| #975 | application.post_init = post_init |
| #976 | |
| #977 | # Add command handlers |
| #978 | application.add_handler(CommandHandler("start", bot.start)) |
| #979 | application.add_handler(CommandHandler("help", bot.help_command)) |
| #980 | application.add_handler(CommandHandler("balance", bot.balance)) |
| #981 | application.add_handler(CommandHandler("portfolio", bot.portfolio)) |
| #982 | application.add_handler(CommandHandler("price", bot.price)) |
| #983 | application.add_handler(CommandHandler("trending", bot.trending)) |
| #984 | application.add_handler(CommandHandler("search", bot.search)) |
| #985 | application.add_handler(CommandHandler("buy", bot.buy)) |
| #986 | application.add_handler(CommandHandler("sell", bot.sell)) |
| #987 | application.add_handler(CommandHandler("quote", bot.quote)) |
| #988 | application.add_handler(CommandHandler("crypto", bot.crypto)) |
| #989 | application.add_handler(CommandHandler("global", bot.global_stats)) |
| #990 | application.add_handler(CommandHandler("trendingcrypto", bot.trending_crypto)) |
| #991 | application.add_handler(CommandHandler("analyze", bot.analyze)) |
| #992 | application.add_handler(CommandHandler("security", bot.security)) |
| #993 | application.add_handler(CommandHandler("cdp_create", bot.cdp_create)) |
| #994 | application.add_handler(CommandHandler("cdp_list", bot.cdp_list)) |
| #995 | application.add_handler(CommandHandler("cdp_balance", bot.cdp_balance)) |
| #996 | application.add_handler(CommandHandler("positions", bot.positions)) |
| #997 | application.add_handler(CommandHandler("long", bot.long_position)) |
| #998 | application.add_handler(CommandHandler("short", bot.short_position)) |
| #999 | application.add_handler(CommandHandler("close", bot.close_position)) |
| #1000 | application.add_handler(CommandHandler("chat", bot.chat)) |
| #1001 | application.add_handler(CommandHandler("browse", bot.browse)) |
| #1002 | |
| #1003 | # Add callback query handler for inline buttons |
| #1004 | application.add_handler(CallbackQueryHandler(bot.button_callback)) |
| #1005 | |
| #1006 | # Add message handler for regular text |
| #1007 | application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, bot.handle_message)) |
| #1008 | |
| #1009 | # Start polling |
| #1010 | print("MAWDBot Telegram is starting...") |
| #1011 | print("Press Ctrl+C to stop") |
| #1012 | application.run_polling(allowed_updates=Update.ALL_TYPES) |
| #1013 | |
| #1014 | |
| #1015 | if __name__ == "__main__": |
| #1016 | main() |
| #1017 |