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 sources16d ago| #1 | """Solana Trading Tools for Mini-Agent""" |
| #2 | |
| #3 | import json |
| #4 | import asyncio |
| #5 | from typing import Any, Optional |
| #6 | from pydantic import BaseModel |
| #7 | |
| #8 | |
| #9 | class ToolResult(BaseModel): |
| #10 | """Tool execution result.""" |
| #11 | success: bool |
| #12 | content: str = "" |
| #13 | error: Optional[str] = None |
| #14 | |
| #15 | |
| #16 | class Tool: |
| #17 | """Base class for all tools.""" |
| #18 | |
| #19 | @property |
| #20 | def name(self) -> str: |
| #21 | raise NotImplementedError |
| #22 | |
| #23 | @property |
| #24 | def description(self) -> str: |
| #25 | raise NotImplementedError |
| #26 | |
| #27 | @property |
| #28 | def parameters(self) -> dict[str, Any]: |
| #29 | raise NotImplementedError |
| #30 | |
| #31 | async def execute(self, *args, **kwargs) -> ToolResult: |
| #32 | raise NotImplementedError |
| #33 | |
| #34 | def to_schema(self) -> dict[str, Any]: |
| #35 | """Convert tool to Anthropic tool schema.""" |
| #36 | return { |
| #37 | "name": self.name, |
| #38 | "description": self.description, |
| #39 | "input_schema": self.parameters, |
| #40 | } |
| #41 | |
| #42 | def to_openai_schema(self) -> dict[str, Any]: |
| #43 | """Convert tool to OpenAI tool schema.""" |
| #44 | return { |
| #45 | "type": "function", |
| #46 | "function": { |
| #47 | "name": self.name, |
| #48 | "description": self.description, |
| #49 | "parameters": self.parameters, |
| #50 | }, |
| #51 | } |
| #52 | |
| #53 | |
| #54 | # Global client instances (initialized by the agent) |
| #55 | _bags_client = None |
| #56 | _jupiter_client = None |
| #57 | _helius_client = None |
| #58 | _birdeye_client = None |
| #59 | _twitter_client = None |
| #60 | _minimax_client = None |
| #61 | _search_client = None |
| #62 | _solana_analyzer = None |
| #63 | _aster_client = None |
| #64 | _hyperliquid_client = None |
| #65 | _cdp_client = None |
| #66 | _coingecko_client = None |
| #67 | _pumpfun_client = None |
| #68 | |
| #69 | |
| #70 | def set_clients(bags_client=None, jupiter_client=None, helius_client=None, birdeye_client=None, twitter_client=None, minimax_client=None, search_client=None, solana_analyzer=None, aster_client=None, hyperliquid_client=None, cdp_client=None, coingecko_client=None, pumpfun_client=None): |
| #71 | """Set the global client instances for tools to use.""" |
| #72 | global _bags_client, _jupiter_client, _helius_client, _birdeye_client, _twitter_client, _minimax_client, _search_client, _solana_analyzer, _aster_client, _hyperliquid_client, _cdp_client, _coingecko_client, _pumpfun_client |
| #73 | _bags_client = bags_client |
| #74 | _jupiter_client = jupiter_client |
| #75 | _helius_client = helius_client |
| #76 | _birdeye_client = birdeye_client |
| #77 | _twitter_client = twitter_client |
| #78 | _minimax_client = minimax_client |
| #79 | _search_client = search_client |
| #80 | _solana_analyzer = solana_analyzer |
| #81 | _aster_client = aster_client |
| #82 | _hyperliquid_client = hyperliquid_client |
| #83 | _cdp_client = cdp_client |
| #84 | _coingecko_client = coingecko_client |
| #85 | _pumpfun_client = pumpfun_client |
| #86 | |
| #87 | |
| #88 | def get_bags_client(): |
| #89 | if _bags_client is None: |
| #90 | raise RuntimeError("BagsClient not initialized. Call set_clients() first.") |
| #91 | return _bags_client |
| #92 | |
| #93 | |
| #94 | def get_jupiter_client(): |
| #95 | if _jupiter_client is None: |
| #96 | raise RuntimeError("JupiterClient not initialized. Call set_clients() first.") |
| #97 | return _jupiter_client |
| #98 | |
| #99 | |
| #100 | def get_helius_client(): |
| #101 | if _helius_client is None: |
| #102 | raise RuntimeError("HeliusClient not initialized. Call set_clients() first.") |
| #103 | return _helius_client |
| #104 | |
| #105 | |
| #106 | def get_birdeye_client(): |
| #107 | if _birdeye_client is None: |
| #108 | raise RuntimeError("BirdeyeClient not initialized. Call set_clients() first.") |
| #109 | return _birdeye_client |
| #110 | |
| #111 | |
| #112 | def get_twitter_client(): |
| #113 | if _twitter_client is None: |
| #114 | raise RuntimeError("TwitterClient not initialized. Call set_clients() first.") |
| #115 | return _twitter_client |
| #116 | |
| #117 | |
| #118 | def get_minimax_client(): |
| #119 | if _minimax_client is None: |
| #120 | raise RuntimeError("MinimaxClient not initialized. Call set_clients() first.") |
| #121 | return _minimax_client |
| #122 | |
| #123 | |
| #124 | def get_search_client(): |
| #125 | if _search_client is None: |
| #126 | raise RuntimeError("SearchAPIClient not initialized. Call set_clients() first.") |
| #127 | return _search_client |
| #128 | |
| #129 | |
| #130 | def get_solana_analyzer(): |
| #131 | if _solana_analyzer is None: |
| #132 | raise RuntimeError("SolanaAnalyzer not initialized. Call set_clients() first.") |
| #133 | return _solana_analyzer |
| #134 | |
| #135 | |
| #136 | def get_aster_client(): |
| #137 | if _aster_client is None: |
| #138 | raise RuntimeError("AsterClient not initialized. Call set_clients() first.") |
| #139 | return _aster_client |
| #140 | |
| #141 | |
| #142 | def get_hyperliquid_client(): |
| #143 | if _hyperliquid_client is None: |
| #144 | raise RuntimeError("HyperliquidClient not initialized. Call set_clients() first.") |
| #145 | return _hyperliquid_client |
| #146 | |
| #147 | |
| #148 | def get_cdp_client(): |
| #149 | """Get CDP client (optional, may return None if not configured).""" |
| #150 | return _cdp_client |
| #151 | |
| #152 | |
| #153 | def get_coingecko_client(): |
| #154 | """Get CoinGecko client (optional, may return None if not configured).""" |
| #155 | return _coingecko_client |
| #156 | |
| #157 | |
| #158 | def get_pumpfun_client(): |
| #159 | """Get PumpFun client (optional, may return None if not configured).""" |
| #160 | return _pumpfun_client |
| #161 | |
| #162 | |
| #163 | class GetWalletBalanceTool(Tool): |
| #164 | """Tool to get wallet SOL and token balances.""" |
| #165 | |
| #166 | @property |
| #167 | def name(self) -> str: |
| #168 | return "get_wallet_balance" |
| #169 | |
| #170 | @property |
| #171 | def description(self) -> str: |
| #172 | return """Get the SOL balance and token holdings for a Solana wallet. |
| #173 | If no wallet address is provided, uses the agent's configured wallet. |
| #174 | Returns SOL balance and list of token balances with USD values.""" |
| #175 | |
| #176 | @property |
| #177 | def parameters(self) -> dict[str, Any]: |
| #178 | return { |
| #179 | "type": "object", |
| #180 | "properties": { |
| #181 | "wallet_address": { |
| #182 | "type": "string", |
| #183 | "description": "The Solana wallet address to check. Optional - uses agent wallet if not provided.", |
| #184 | } |
| #185 | }, |
| #186 | "required": [], |
| #187 | } |
| #188 | |
| #189 | async def execute(self, wallet_address: str = None) -> ToolResult: |
| #190 | try: |
| #191 | helius = get_helius_client() |
| #192 | bags = get_bags_client() |
| #193 | |
| #194 | # Use provided address or agent's wallet |
| #195 | address = wallet_address or bags.wallet_pubkey |
| #196 | if not address: |
| #197 | return ToolResult(success=False, error="No wallet address provided and no agent wallet configured") |
| #198 | |
| #199 | # Get SOL balance |
| #200 | sol_balance = await helius.get_sol_balance(address) |
| #201 | |
| #202 | # Get token balances |
| #203 | token_balances = await helius.get_token_accounts_by_owner(address) |
| #204 | |
| #205 | result = { |
| #206 | "wallet": address, |
| #207 | "sol_balance": sol_balance, |
| #208 | "tokens": [ |
| #209 | { |
| #210 | "mint": tb.mint, |
| #211 | "amount": tb.ui_amount, |
| #212 | "decimals": tb.decimals, |
| #213 | } |
| #214 | for tb in token_balances |
| #215 | if tb.ui_amount > 0 |
| #216 | ] |
| #217 | } |
| #218 | |
| #219 | return ToolResult( |
| #220 | success=True, |
| #221 | content=json.dumps(result, indent=2) |
| #222 | ) |
| #223 | except Exception as e: |
| #224 | return ToolResult(success=False, error=str(e)) |
| #225 | |
| #226 | |
| #227 | class GetTokenPriceTool(Tool): |
| #228 | """Tool to get current token price.""" |
| #229 | |
| #230 | @property |
| #231 | def name(self) -> str: |
| #232 | return "get_token_price" |
| #233 | |
| #234 | @property |
| #235 | def description(self) -> str: |
| #236 | return """Get the current price and 24h change for a Solana token. |
| #237 | Requires the token mint address. |
| #238 | Returns price in USD, 24h price change percentage, volume, and liquidity.""" |
| #239 | |
| #240 | @property |
| #241 | def parameters(self) -> dict[str, Any]: |
| #242 | return { |
| #243 | "type": "object", |
| #244 | "properties": { |
| #245 | "token_mint": { |
| #246 | "type": "string", |
| #247 | "description": "The token mint address to get price for.", |
| #248 | } |
| #249 | }, |
| #250 | "required": ["token_mint"], |
| #251 | } |
| #252 | |
| #253 | async def execute(self, token_mint: str) -> ToolResult: |
| #254 | try: |
| #255 | birdeye = get_birdeye_client() |
| #256 | |
| #257 | price_data = await birdeye.get_token_price(token_mint) |
| #258 | |
| #259 | result = { |
| #260 | "mint": token_mint, |
| #261 | "price_usd": price_data.get("value", 0), |
| #262 | "price_change_24h": price_data.get("priceChange24h", 0), |
| #263 | } |
| #264 | |
| #265 | return ToolResult( |
| #266 | success=True, |
| #267 | content=json.dumps(result, indent=2) |
| #268 | ) |
| #269 | except Exception as e: |
| #270 | return ToolResult(success=False, error=str(e)) |
| #271 | |
| #272 | |
| #273 | class GetTokenInfoTool(Tool): |
| #274 | """Tool to get comprehensive token information.""" |
| #275 | |
| #276 | @property |
| #277 | def name(self) -> str: |
| #278 | return "get_token_info" |
| #279 | |
| #280 | @property |
| #281 | def description(self) -> str: |
| #282 | return """Get comprehensive information about a Solana token including: |
| #283 | - Name, symbol, decimals |
| #284 | - Current price and price changes |
| #285 | - Market cap, volume, liquidity |
| #286 | - Holder count |
| #287 | - Security info |
| #288 | Requires the token mint address.""" |
| #289 | |
| #290 | @property |
| #291 | def parameters(self) -> dict[str, Any]: |
| #292 | return { |
| #293 | "type": "object", |
| #294 | "properties": { |
| #295 | "token_mint": { |
| #296 | "type": "string", |
| #297 | "description": "The token mint address to get info for.", |
| #298 | } |
| #299 | }, |
| #300 | "required": ["token_mint"], |
| #301 | } |
| #302 | |
| #303 | async def execute(self, token_mint: str) -> ToolResult: |
| #304 | try: |
| #305 | birdeye = get_birdeye_client() |
| #306 | |
| #307 | overview = await birdeye.get_token_overview(token_mint) |
| #308 | |
| #309 | result = { |
| #310 | "mint": overview.mint, |
| #311 | "name": overview.name, |
| #312 | "symbol": overview.symbol, |
| #313 | "decimals": overview.decimals, |
| #314 | "price_usd": overview.price, |
| #315 | "price_change_1h": overview.price_change_1h, |
| #316 | "price_change_24h": overview.price_change_24h, |
| #317 | "volume_24h": overview.volume_24h, |
| #318 | "liquidity": overview.liquidity, |
| #319 | "market_cap": overview.market_cap, |
| #320 | "supply": overview.supply, |
| #321 | "holder_count": overview.holder_count, |
| #322 | } |
| #323 | |
| #324 | # Try to get security info |
| #325 | try: |
| #326 | security = await birdeye.get_token_security(token_mint) |
| #327 | result["security"] = security |
| #328 | except: |
| #329 | pass |
| #330 | |
| #331 | return ToolResult( |
| #332 | success=True, |
| #333 | content=json.dumps(result, indent=2) |
| #334 | ) |
| #335 | except Exception as e: |
| #336 | return ToolResult(success=False, error=str(e)) |
| #337 | |
| #338 | |
| #339 | class GetSwapQuoteTool(Tool): |
| #340 | """Tool to get a swap quote without executing.""" |
| #341 | |
| #342 | @property |
| #343 | def name(self) -> str: |
| #344 | return "get_swap_quote" |
| #345 | |
| #346 | @property |
| #347 | def description(self) -> str: |
| #348 | return """Get a quote for swapping tokens without executing the trade. |
| #349 | Use this to check expected output amounts and price impact before trading. |
| #350 | Returns the quote with expected output, minimum output, price impact, and route.""" |
| #351 | |
| #352 | @property |
| #353 | def parameters(self) -> dict[str, Any]: |
| #354 | return { |
| #355 | "type": "object", |
| #356 | "properties": { |
| #357 | "input_mint": { |
| #358 | "type": "string", |
| #359 | "description": "The input token mint address. Use 'So11111111111111111111111111111111111111112' for SOL.", |
| #360 | }, |
| #361 | "output_mint": { |
| #362 | "type": "string", |
| #363 | "description": "The output token mint address. Use 'So11111111111111111111111111111111111111112' for SOL.", |
| #364 | }, |
| #365 | "amount": { |
| #366 | "type": "number", |
| #367 | "description": "Amount to swap in the smallest unit (lamports for SOL, or raw token amount).", |
| #368 | }, |
| #369 | "slippage_bps": { |
| #370 | "type": "integer", |
| #371 | "description": "Slippage tolerance in basis points (100 = 1%). Default is 300 (3%).", |
| #372 | "default": 300, |
| #373 | }, |
| #374 | }, |
| #375 | "required": ["input_mint", "output_mint", "amount"], |
| #376 | } |
| #377 | |
| #378 | async def execute( |
| #379 | self, |
| #380 | input_mint: str, |
| #381 | output_mint: str, |
| #382 | amount: int, |
| #383 | slippage_bps: int = 300, |
| #384 | ) -> ToolResult: |
| #385 | try: |
| #386 | bags = get_bags_client() |
| #387 | |
| #388 | quote = await bags.get_quote( |
| #389 | input_mint=input_mint, |
| #390 | output_mint=output_mint, |
| #391 | amount=int(amount), |
| #392 | slippage_bps=slippage_bps, |
| #393 | ) |
| #394 | |
| #395 | result = { |
| #396 | "input_mint": quote.input_mint, |
| #397 | "output_mint": quote.output_mint, |
| #398 | "input_amount": quote.in_amount, |
| #399 | "output_amount": quote.out_amount, |
| #400 | "min_output_amount": quote.min_out_amount, |
| #401 | "price_impact_pct": quote.price_impact_pct, |
| #402 | "slippage_bps": quote.slippage_bps, |
| #403 | "route_plan": [ |
| #404 | {"venue": leg.get("venue"), "input_mint": leg.get("inputMint"), "output_mint": leg.get("outputMint")} |
| #405 | for leg in quote.route_plan |
| #406 | ], |
| #407 | } |
| #408 | |
| #409 | return ToolResult( |
| #410 | success=True, |
| #411 | content=json.dumps(result, indent=2) |
| #412 | ) |
| #413 | except Exception as e: |
| #414 | return ToolResult(success=False, error=str(e)) |
| #415 | |
| #416 | |
| #417 | class BuyTokenTool(Tool): |
| #418 | """Tool to buy a token with SOL.""" |
| #419 | |
| #420 | @property |
| #421 | def name(self) -> str: |
| #422 | return "buy_token" |
| #423 | |
| #424 | @property |
| #425 | def description(self) -> str: |
| #426 | return """Buy a Solana token using SOL from the agent's wallet. |
| #427 | WARNING: This executes a real trade and spends real SOL. |
| #428 | Returns the transaction signature, amount spent, and tokens received.""" |
| #429 | |
| #430 | @property |
| #431 | def parameters(self) -> dict[str, Any]: |
| #432 | return { |
| #433 | "type": "object", |
| #434 | "properties": { |
| #435 | "token_mint": { |
| #436 | "type": "string", |
| #437 | "description": "The mint address of the token to buy.", |
| #438 | }, |
| #439 | "sol_amount": { |
| #440 | "type": "number", |
| #441 | "description": "Amount of SOL to spend on the purchase.", |
| #442 | }, |
| #443 | "slippage_bps": { |
| #444 | "type": "integer", |
| #445 | "description": "Slippage tolerance in basis points (100 = 1%). Default is 300 (3%).", |
| #446 | "default": 300, |
| #447 | }, |
| #448 | }, |
| #449 | "required": ["token_mint", "sol_amount"], |
| #450 | } |
| #451 | |
| #452 | async def execute( |
| #453 | self, |
| #454 | token_mint: str, |
| #455 | sol_amount: float, |
| #456 | slippage_bps: int = 300, |
| #457 | ) -> ToolResult: |
| #458 | try: |
| #459 | # Prefer Jupiter, fallback to Bags |
| #460 | if _jupiter_client is not None and _jupiter_client.keypair is not None: |
| #461 | signature = await _jupiter_client.buy_token( |
| #462 | token_mint=token_mint, |
| #463 | sol_amount=sol_amount, |
| #464 | slippage_bps=slippage_bps, |
| #465 | ) |
| #466 | |
| #467 | return ToolResult( |
| #468 | success=True, |
| #469 | content=json.dumps({ |
| #470 | "status": "success", |
| #471 | "transaction_signature": signature, |
| #472 | "sol_spent": sol_amount, |
| #473 | "trading_api": "jupiter_ultra", |
| #474 | }, indent=2) |
| #475 | ) |
| #476 | elif _bags_client is not None: |
| #477 | # Fallback to Bags (legacy) |
| #478 | if _bags_client.keypair is None: |
| #479 | return ToolResult( |
| #480 | success=False, |
| #481 | error="No wallet configured for trading. Private key required." |
| #482 | ) |
| #483 | |
| #484 | result = await _bags_client.buy_token( |
| #485 | token_mint=token_mint, |
| #486 | sol_amount=sol_amount, |
| #487 | slippage_bps=slippage_bps, |
| #488 | ) |
| #489 | |
| #490 | return ToolResult( |
| #491 | success=True, |
| #492 | content=json.dumps({ |
| #493 | "status": "success", |
| #494 | "transaction_signature": result["signature"], |
| #495 | "sol_spent": result["quote"]["in_amount"], |
| #496 | "tokens_received": result["quote"]["out_amount"], |
| #497 | "min_tokens_expected": result["quote"]["min_out_amount"], |
| #498 | "price_impact": result["quote"]["price_impact_pct"], |
| #499 | "trading_api": "bags", |
| #500 | }, indent=2) |
| #501 | ) |
| #502 | else: |
| #503 | return ToolResult( |
| #504 | success=False, |
| #505 | error="No trading API configured. Please configure Jupiter or Bags API with a valid private key." |
| #506 | ) |
| #507 | except Exception as e: |
| #508 | return ToolResult(success=False, error=str(e)) |
| #509 | |
| #510 | |
| #511 | class SellTokenTool(Tool): |
| #512 | """Tool to sell a token for SOL.""" |
| #513 | |
| #514 | @property |
| #515 | def name(self) -> str: |
| #516 | return "sell_token" |
| #517 | |
| #518 | @property |
| #519 | def description(self) -> str: |
| #520 | return """Sell a Solana token for SOL. |
| #521 | WARNING: This executes a real trade. |
| #522 | Returns the transaction signature, tokens sold, and SOL received.""" |
| #523 | |
| #524 | @property |
| #525 | def parameters(self) -> dict[str, Any]: |
| #526 | return { |
| #527 | "type": "object", |
| #528 | "properties": { |
| #529 | "token_mint": { |
| #530 | "type": "string", |
| #531 | "description": "The mint address of the token to sell.", |
| #532 | }, |
| #533 | "token_amount": { |
| #534 | "type": "integer", |
| #535 | "description": "Amount of tokens to sell (in smallest unit/raw amount).", |
| #536 | }, |
| #537 | "slippage_bps": { |
| #538 | "type": "integer", |
| #539 | "description": "Slippage tolerance in basis points (100 = 1%). Default is 300 (3%).", |
| #540 | "default": 300, |
| #541 | }, |
| #542 | }, |
| #543 | "required": ["token_mint", "token_amount"], |
| #544 | } |
| #545 | |
| #546 | async def execute( |
| #547 | self, |
| #548 | token_mint: str, |
| #549 | token_amount: int, |
| #550 | slippage_bps: int = 300, |
| #551 | ) -> ToolResult: |
| #552 | try: |
| #553 | # Prefer Jupiter, fallback to Bags |
| #554 | if _jupiter_client is not None and _jupiter_client.keypair is not None: |
| #555 | signature = await _jupiter_client.sell_token( |
| #556 | token_mint=token_mint, |
| #557 | token_amount=int(token_amount), |
| #558 | slippage_bps=slippage_bps, |
| #559 | ) |
| #560 | |
| #561 | return ToolResult( |
| #562 | success=True, |
| #563 | content=json.dumps({ |
| #564 | "status": "success", |
| #565 | "transaction_signature": signature, |
| #566 | "tokens_sold": token_amount, |
| #567 | "trading_api": "jupiter_ultra", |
| #568 | }, indent=2) |
| #569 | ) |
| #570 | elif _bags_client is not None: |
| #571 | # Fallback to Bags (legacy) |
| #572 | if _bags_client.keypair is None: |
| #573 | return ToolResult( |
| #574 | success=False, |
| #575 | error="No wallet configured for trading. Private key required." |
| #576 | ) |
| #577 | |
| #578 | result = await _bags_client.sell_token( |
| #579 | token_mint=token_mint, |
| #580 | token_amount=int(token_amount), |
| #581 | slippage_bps=slippage_bps, |
| #582 | ) |
| #583 | |
| #584 | return ToolResult( |
| #585 | success=True, |
| #586 | content=json.dumps({ |
| #587 | "status": "success", |
| #588 | "transaction_signature": result["signature"], |
| #589 | "tokens_sold": result["quote"]["in_amount"], |
| #590 | "sol_received": result["quote"]["out_amount"], |
| #591 | "min_sol_expected": result["quote"]["min_out_amount"], |
| #592 | "price_impact": result["quote"]["price_impact_pct"], |
| #593 | "trading_api": "bags", |
| #594 | }, indent=2) |
| #595 | ) |
| #596 | else: |
| #597 | return ToolResult( |
| #598 | success=False, |
| #599 | error="No trading API configured. Please configure Jupiter or Bags API with a valid private key." |
| #600 | ) |
| #601 | except Exception as e: |
| #602 | return ToolResult(success=False, error=str(e)) |
| #603 | |
| #604 | |
| #605 | class GetPortfolioTool(Tool): |
| #606 | """Tool to get wallet portfolio with USD values.""" |
| #607 | |
| #608 | @property |
| #609 | def name(self) -> str: |
| #610 | return "get_portfolio" |
| #611 | |
| #612 | @property |
| #613 | def description(self) -> str: |
| #614 | return """Get the complete portfolio for a wallet including: |
| #615 | - All token holdings with current USD values |
| #616 | - Total portfolio value |
| #617 | If no wallet is provided, uses the agent's wallet.""" |
| #618 | |
| #619 | @property |
| #620 | def parameters(self) -> dict[str, Any]: |
| #621 | return { |
| #622 | "type": "object", |
| #623 | "properties": { |
| #624 | "wallet_address": { |
| #625 | "type": "string", |
| #626 | "description": "The wallet address. Optional - uses agent wallet if not provided.", |
| #627 | } |
| #628 | }, |
| #629 | "required": [], |
| #630 | } |
| #631 | |
| #632 | async def execute(self, wallet_address: str = None) -> ToolResult: |
| #633 | try: |
| #634 | birdeye = get_birdeye_client() |
| #635 | bags = get_bags_client() |
| #636 | |
| #637 | address = wallet_address or bags.wallet_pubkey |
| #638 | if not address: |
| #639 | return ToolResult(success=False, error="No wallet address provided") |
| #640 | |
| #641 | portfolio = await birdeye.get_wallet_portfolio(address) |
| #642 | |
| #643 | return ToolResult( |
| #644 | success=True, |
| #645 | content=json.dumps({ |
| #646 | "wallet": address, |
| #647 | "portfolio": portfolio, |
| #648 | }, indent=2) |
| #649 | ) |
| #650 | except Exception as e: |
| #651 | return ToolResult(success=False, error=str(e)) |
| #652 | |
| #653 | |
| #654 | class GetTrendingTokensTool(Tool): |
| #655 | """Tool to get trending tokens.""" |
| #656 | |
| #657 | @property |
| #658 | def name(self) -> str: |
| #659 | return "get_trending_tokens" |
| #660 | |
| #661 | @property |
| #662 | def description(self) -> str: |
| #663 | return """Get a list of trending Solana tokens sorted by various metrics. |
| #664 | Returns token names, symbols, prices, volumes, and price changes.""" |
| #665 | |
| #666 | @property |
| #667 | def parameters(self) -> dict[str, Any]: |
| #668 | return { |
| #669 | "type": "object", |
| #670 | "properties": { |
| #671 | "sort_by": { |
| #672 | "type": "string", |
| #673 | "description": "Sort by: 'rank', 'volume24h', 'liquidity', 'price'. Default is 'volume24h'.", |
| #674 | "default": "volume24h", |
| #675 | }, |
| #676 | "limit": { |
| #677 | "type": "integer", |
| #678 | "description": "Number of tokens to return. Default is 20.", |
| #679 | "default": 20, |
| #680 | }, |
| #681 | }, |
| #682 | "required": [], |
| #683 | } |
| #684 | |
| #685 | async def execute( |
| #686 | self, |
| #687 | sort_by: str = "volume24h", |
| #688 | limit: int = 20, |
| #689 | ) -> ToolResult: |
| #690 | try: |
| #691 | birdeye = get_birdeye_client() |
| #692 | |
| #693 | tokens = await birdeye.get_trending_tokens( |
| #694 | sort_by=sort_by, |
| #695 | sort_type="desc", |
| #696 | limit=limit, |
| #697 | ) |
| #698 | |
| #699 | return ToolResult( |
| #700 | success=True, |
| #701 | content=json.dumps({ |
| #702 | "trending_tokens": tokens, |
| #703 | "sort_by": sort_by, |
| #704 | "count": len(tokens) if isinstance(tokens, list) else 0, |
| #705 | }, indent=2) |
| #706 | ) |
| #707 | except Exception as e: |
| #708 | return ToolResult(success=False, error=str(e)) |
| #709 | |
| #710 | |
| #711 | class SearchTokenTool(Tool): |
| #712 | """Tool to search for tokens by name or symbol.""" |
| #713 | |
| #714 | @property |
| #715 | def name(self) -> str: |
| #716 | return "search_token" |
| #717 | |
| #718 | @property |
| #719 | def description(self) -> str: |
| #720 | return """Search for Solana tokens by name or symbol. |
| #721 | Returns matching tokens with their mint addresses, names, symbols, and basic info.""" |
| #722 | |
| #723 | @property |
| #724 | def parameters(self) -> dict[str, Any]: |
| #725 | return { |
| #726 | "type": "object", |
| #727 | "properties": { |
| #728 | "query": { |
| #729 | "type": "string", |
| #730 | "description": "Search query (token name or symbol).", |
| #731 | }, |
| #732 | "limit": { |
| #733 | "type": "integer", |
| #734 | "description": "Maximum results to return. Default is 10.", |
| #735 | "default": 10, |
| #736 | }, |
| #737 | }, |
| #738 | "required": ["query"], |
| #739 | } |
| #740 | |
| #741 | async def execute(self, query: str, limit: int = 10) -> ToolResult: |
| #742 | try: |
| #743 | birdeye = get_birdeye_client() |
| #744 | |
| #745 | results = await birdeye.search_token(query, limit=limit) |
| #746 | |
| #747 | return ToolResult( |
| #748 | success=True, |
| #749 | content=json.dumps({ |
| #750 | "query": query, |
| #751 | "results": results, |
| #752 | "count": len(results) if isinstance(results, list) else 0, |
| #753 | }, indent=2) |
| #754 | ) |
| #755 | except Exception as e: |
| #756 | return ToolResult(success=False, error=str(e)) |
| #757 | |
| #758 | |
| #759 | class GetWalletNetWorthTool(Tool): |
| #760 | """Tool to get current net worth and detailed portfolio for a wallet.""" |
| #761 | |
| #762 | @property |
| #763 | def name(self) -> str: |
| #764 | return "get_wallet_net_worth" |
| #765 | |
| #766 | @property |
| #767 | def description(self) -> str: |
| #768 | return """Get the current net worth and detailed portfolio breakdown for a wallet. |
| #769 | Returns total USD value, list of all assets with prices, balances, and individual values. |
| #770 | Perfect for checking overall portfolio value and seeing which assets you hold.""" |
| #771 | |
| #772 | @property |
| #773 | def parameters(self) -> dict[str, Any]: |
| #774 | return { |
| #775 | "type": "object", |
| #776 | "properties": { |
| #777 | "wallet_address": { |
| #778 | "type": "string", |
| #779 | "description": "The wallet address to check. Optional - uses agent wallet if not provided.", |
| #780 | }, |
| #781 | "min_value": { |
| #782 | "type": "number", |
| #783 | "description": "Optional: Only show assets worth at least this much USD", |
| #784 | }, |
| #785 | }, |
| #786 | "required": [], |
| #787 | } |
| #788 | |
| #789 | async def execute(self, wallet_address: str = None, min_value: float = None) -> ToolResult: |
| #790 | try: |
| #791 | birdeye = get_birdeye_client() |
| #792 | bags = get_bags_client() |
| #793 | |
| #794 | # Use provided address or agent's wallet |
| #795 | address = wallet_address or bags.wallet_pubkey |
| #796 | if not address: |
| #797 | return ToolResult(success=False, error="No wallet address provided") |
| #798 | |
| #799 | # Get net worth data |
| #800 | data = await birdeye.get_wallet_net_worth( |
| #801 | wallet=address, |
| #802 | filter_value=min_value, |
| #803 | limit=100 |
| #804 | ) |
| #805 | |
| #806 | result = { |
| #807 | "wallet": data.get("wallet_address", address), |
| #808 | "total_net_worth_usd": float(data.get("total_value", 0)), |
| #809 | "currency": data.get("currency", "usd"), |
| #810 | "timestamp": data.get("current_timestamp"), |
| #811 | "asset_count": len(data.get("items", [])), |
| #812 | "assets": [ |
| #813 | { |
| #814 | "symbol": item.get("symbol"), |
| #815 | "name": item.get("name"), |
| #816 | "address": item.get("address"), |
| #817 | "balance": float(item.get("amount", 0)), |
| #818 | "price_usd": float(item.get("price", 0)), |
| #819 | "value_usd": float(item.get("value", 0)), |
| #820 | "logo": item.get("logo_uri"), |
| #821 | } |
| #822 | for item in data.get("items", [])[:20] # Limit to top 20 for readability |
| #823 | ] |
| #824 | } |
| #825 | |
| #826 | return ToolResult( |
| #827 | success=True, |
| #828 | content=json.dumps(result, indent=2) |
| #829 | ) |
| #830 | except Exception as e: |
| #831 | return ToolResult(success=False, error=str(e)) |
| #832 | |
| #833 | |
| #834 | class GetWalletNetWorthChartTool(Tool): |
| #835 | """Tool to get historical net worth chart data for a wallet.""" |
| #836 | |
| #837 | @property |
| #838 | def name(self) -> str: |
| #839 | return "get_wallet_net_worth_chart" |
| #840 | |
| #841 | @property |
| #842 | def description(self) -> str: |
| #843 | return """Get historical net worth data to see how wallet value has changed over time. |
| #844 | Returns daily or hourly net worth history showing gains/losses. |
| #845 | Useful for tracking portfolio performance over days or weeks.""" |
| #846 | |
| #847 | @property |
| #848 | def parameters(self) -> dict[str, Any]: |
| #849 | return { |
| #850 | "type": "object", |
| #851 | "properties": { |
| #852 | "wallet_address": { |
| #853 | "type": "string", |
| #854 | "description": "The wallet address to check. Optional - uses agent wallet if not provided.", |
| #855 | }, |
| #856 | "days": { |
| #857 | "type": "integer", |
| #858 | "description": "Number of days of history (1-30, default: 7)", |
| #859 | }, |
| #860 | }, |
| #861 | "required": [], |
| #862 | } |
| #863 | |
| #864 | async def execute(self, wallet_address: str = None, days: int = 7) -> ToolResult: |
| #865 | try: |
| #866 | birdeye = get_birdeye_client() |
| #867 | bags = get_bags_client() |
| #868 | |
| #869 | # Use provided address or agent's wallet |
| #870 | address = wallet_address or bags.wallet_pubkey |
| #871 | if not address: |
| #872 | return ToolResult(success=False, error="No wallet address provided") |
| #873 | |
| #874 | # Limit days to 1-30 |
| #875 | days = max(1, min(30, days)) |
| #876 | |
| #877 | # Get net worth chart |
| #878 | data = await birdeye.get_wallet_net_worth_chart( |
| #879 | wallet=address, |
| #880 | count=days, |
| #881 | direction="back", |
| #882 | time_type="1d" |
| #883 | ) |
| #884 | |
| #885 | history = data.get("history", []) |
| #886 | |
| #887 | result = { |
| #888 | "wallet": data.get("wallet_address", address), |
| #889 | "currency": data.get("currency", "usd"), |
| #890 | "current_timestamp": data.get("current_timestamp"), |
| #891 | "past_timestamp": data.get("past_timestamp"), |
| #892 | "history_points": len(history), |
| #893 | "history": [ |
| #894 | { |
| #895 | "timestamp": item.get("timestamp"), |
| #896 | "net_worth": float(item.get("net_worth", 0)), |
| #897 | "change": float(item.get("net_worth_change", 0)), |
| #898 | "change_percent": float(item.get("net_worth_change_percent", 0)), |
| #899 | } |
| #900 | for item in history |
| #901 | ] |
| #902 | } |
| #903 | |
| #904 | return ToolResult( |
| #905 | success=True, |
| #906 | content=json.dumps(result, indent=2) |
| #907 | ) |
| #908 | except Exception as e: |
| #909 | return ToolResult(success=False, error=str(e)) |
| #910 | |
| #911 | |
| #912 | class GetWalletPnLTool(Tool): |
| #913 | """Tool to get Profit & Loss (PnL) data for a wallet.""" |
| #914 | |
| #915 | @property |
| #916 | def name(self) -> str: |
| #917 | return "get_wallet_pnl" |
| #918 | |
| #919 | @property |
| #920 | def description(self) -> str: |
| #921 | return """Get comprehensive Profit & Loss (PnL) data for a wallet. |
| #922 | Shows realized profit (from completed trades), unrealized profit (from current holdings), |
| #923 | total trades, win rate, average profit per trade, and more trading statistics. |
| #924 | Essential for understanding trading performance.""" |
| #925 | |
| #926 | @property |
| #927 | def parameters(self) -> dict[str, Any]: |
| #928 | return { |
| #929 | "type": "object", |
| #930 | "properties": { |
| #931 | "wallet_address": { |
| #932 | "type": "string", |
| #933 | "description": "The wallet address to check. Optional - uses agent wallet if not provided.", |
| #934 | }, |
| #935 | "duration": { |
| #936 | "type": "string", |
| #937 | "description": "Time period: 'all' (default), '90d', '30d', '7d', '24h'", |
| #938 | "enum": ["all", "90d", "30d", "7d", "24h"], |
| #939 | }, |
| #940 | }, |
| #941 | "required": [], |
| #942 | } |
| #943 | |
| #944 | async def execute(self, wallet_address: str = None, duration: str = "all") -> ToolResult: |
| #945 | try: |
| #946 | birdeye = get_birdeye_client() |
| #947 | bags = get_bags_client() |
| #948 | |
| #949 | # Use provided address or agent's wallet |
| #950 | address = wallet_address or bags.wallet_pubkey |
| #951 | if not address: |
| #952 | return ToolResult(success=False, error="No wallet address provided") |
| #953 | |
| #954 | # Get PnL summary |
| #955 | data = await birdeye.get_wallet_pnl_summary(wallet=address, duration=duration) |
| #956 | summary = data.get("summary", {}) |
| #957 | |
| #958 | counts = summary.get("counts", {}) |
| #959 | cashflow = summary.get("cashflow_usd", {}) |
| #960 | pnl = summary.get("pnl", {}) |
| #961 | |
| #962 | result = { |
| #963 | "wallet": address, |
| #964 | "duration": duration, |
| #965 | "unique_tokens_traded": summary.get("unique_tokens", 0), |
| #966 | "trading_stats": { |
| #967 | "total_trades": counts.get("total_trade", 0), |
| #968 | "buy_trades": counts.get("total_buy", 0), |
| #969 | "sell_trades": counts.get("total_sell", 0), |
| #970 | "winning_trades": counts.get("total_win", 0), |
| #971 | "losing_trades": counts.get("total_loss", 0), |
| #972 | "win_rate_percent": float(counts.get("win_rate", 0)) * 100, |
| #973 | }, |
| #974 | "cashflow": { |
| #975 | "total_invested_usd": float(cashflow.get("total_invested", 0)), |
| #976 | "total_sold_usd": float(cashflow.get("total_sold", 0)), |
| #977 | }, |
| #978 | "profit_loss": { |
| #979 | "realized_profit_usd": float(pnl.get("realized_profit_usd", 0)), |
| #980 | "realized_profit_percent": float(pnl.get("realized_profit_percent", 0)) * 100, |
| #981 | "unrealized_profit_usd": float(pnl.get("unrealized_usd", 0)), |
| #982 | "total_profit_usd": float(pnl.get("total_usd", 0)), |
| #983 | "avg_profit_per_trade_usd": float(pnl.get("avg_profit_per_trade_usd", 0)), |
| #984 | } |
| #985 | } |
| #986 | |
| #987 | return ToolResult( |
| #988 | success=True, |
| #989 | content=json.dumps(result, indent=2) |
| #990 | ) |
| #991 | except Exception as e: |
| #992 | return ToolResult(success=False, error=str(e)) |
| #993 | |
| #994 | |
| #995 | class GetTokenChartTool(Tool): |
| #996 | """Tool to get OHLCV chart data for a token.""" |
| #997 | |
| #998 | @property |
| #999 | def name(self) -> str: |
| #1000 | return "get_token_chart" |
| #1001 | |
| #1002 | @property |
| #1003 | def description(self) -> str: |
| #1004 | return """Get OHLCV (Open, High, Low, Close, Volume) candlestick chart data for a token. |
| #1005 | Shows price action and trading volume over time. |
| #1006 | Perfect for technical analysis and price trend visualization. |
| #1007 | Supports multiple timeframes from 1 minute to 1 month.""" |
| #1008 | |
| #1009 | @property |
| #1010 | def parameters(self) -> dict[str, Any]: |
| #1011 | return { |
| #1012 | "type": "object", |
| #1013 | "properties": { |
| #1014 | "token_address": { |
| #1015 | "type": "string", |
| #1016 | "description": "The Solana token mint address", |
| #1017 | }, |
| #1018 | "timeframe": { |
| #1019 | "type": "string", |
| #1020 | "description": "Chart timeframe: '1m', '5m', '15m', '1H', '4H', '1D', '1W'. Default: '15m'", |
| #1021 | "enum": ["1m", "3m", "5m", "15m", "30m", "1H", "2H", "4H", "6H", "8H", "12H", "1D", "3D", "1W", "1M"], |
| #1022 | }, |
| #1023 | "hours": { |
| #1024 | "type": "integer", |
| #1025 | "description": "Number of hours of data to fetch (default: 24)", |
| #1026 | }, |
| #1027 | }, |
| #1028 | "required": ["token_address"], |
| #1029 | } |
| #1030 | |
| #1031 | async def execute( |
| #1032 | self, |
| #1033 | token_address: str, |
| #1034 | timeframe: str = "15m", |
| #1035 | hours: int = 24 |
| #1036 | ) -> ToolResult: |
| #1037 | try: |
| #1038 | birdeye = get_birdeye_client() |
| #1039 | import time |
| #1040 | |
| #1041 | # Calculate timestamps |
| #1042 | time_to = int(time.time()) |
| #1043 | time_from = time_to - (hours * 3600) |
| #1044 | |
| #1045 | # Get OHLCV data |
| #1046 | data = await birdeye.get_ohlcv( |
| #1047 | mint=token_address, |
| #1048 | time_type=timeframe, |
| #1049 | time_from=time_from, |
| #1050 | time_to=time_to |
| #1051 | ) |
| #1052 | |
| #1053 | if not data: |
| #1054 | return ToolResult( |
| #1055 | success=False, |
| #1056 | error="No chart data available for this token" |
| #1057 | ) |
| #1058 | |
| #1059 | # Format chart data |
| #1060 | candles = [] |
| #1061 | for candle in data: |
| #1062 | candles.append({ |
| #1063 | "timestamp": candle.timestamp, |
| #1064 | "datetime": time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(candle.timestamp)), |
| #1065 | "open": candle.open, |
| #1066 | "high": candle.high, |
| #1067 | "low": candle.low, |
| #1068 | "close": candle.close, |
| #1069 | "volume": candle.volume, |
| #1070 | }) |
| #1071 | |
| #1072 | # Calculate some stats |
| #1073 | if candles: |
| #1074 | first_price = candles[0]["open"] |
| #1075 | last_price = candles[-1]["close"] |
| #1076 | price_change = last_price - first_price |
| #1077 | price_change_pct = (price_change / first_price * 100) if first_price > 0 else 0 |
| #1078 | high_price = max(c["high"] for c in candles) |
| #1079 | low_price = min(c["low"] for c in candles) |
| #1080 | total_volume = sum(c["volume"] for c in candles) |
| #1081 | |
| #1082 | result = { |
| #1083 | "token_address": token_address, |
| #1084 | "timeframe": timeframe, |
| #1085 | "hours": hours, |
| #1086 | "candle_count": len(candles), |
| #1087 | "period_stats": { |
| #1088 | "first_price": first_price, |
| #1089 | "last_price": last_price, |
| #1090 | "price_change": price_change, |
| #1091 | "price_change_percent": price_change_pct, |
| #1092 | "high": high_price, |
| #1093 | "low": low_price, |
| #1094 | "total_volume": total_volume, |
| #1095 | }, |
| #1096 | "candles": candles[:50] # Limit to 50 for readability |
| #1097 | } |
| #1098 | |
| #1099 | return ToolResult( |
| #1100 | success=True, |
| #1101 | content=json.dumps(result, indent=2) |
| #1102 | ) |
| #1103 | else: |
| #1104 | return ToolResult( |
| #1105 | success=False, |
| #1106 | error="No candle data found" |
| #1107 | ) |
| #1108 | |
| #1109 | except Exception as e: |
| #1110 | return ToolResult(success=False, error=f"Failed to get chart data: {str(e)}") |
| #1111 | |
| #1112 | |
| #1113 | class AnalyzeTokenSecurityTool(Tool): |
| #1114 | """Tool to analyze token security and get comprehensive token information.""" |
| #1115 | |
| #1116 | @property |
| #1117 | def name(self) -> str: |
| #1118 | return "analyze_token_security" |
| #1119 | |
| #1120 | @property |
| #1121 | def description(self) -> str: |
| #1122 | return """Analyze any Solana token contract for security risks, ownership, creation info, and comprehensive data. |
| #1123 | Paste any token address to get: |
| #1124 | - Security analysis (freeze authority, mint authority, rug pull risks) |
| #1125 | - Creator information and creation timestamp |
| #1126 | - Token metadata and social links |
| #1127 | - Price, volume, liquidity, market cap |
| #1128 | - Holder count and distribution |
| #1129 | Use this to evaluate tokens before trading or investing.""" |
| #1130 | |
| #1131 | @property |
| #1132 | def parameters(self) -> dict[str, Any]: |
| #1133 | return { |
| #1134 | "type": "object", |
| #1135 | "properties": { |
| #1136 | "token_address": { |
| #1137 | "type": "string", |
| #1138 | "description": "The Solana token mint address to analyze", |
| #1139 | }, |
| #1140 | }, |
| #1141 | "required": ["token_address"], |
| #1142 | } |
| #1143 | |
| #1144 | async def execute(self, token_address: str) -> ToolResult: |
| #1145 | try: |
| #1146 | birdeye = get_birdeye_client() |
| #1147 | |
| #1148 | # Gather comprehensive token data in parallel |
| #1149 | security_task = birdeye.get_token_security(token_address) |
| #1150 | creation_task = birdeye.get_token_creation_info(token_address) |
| #1151 | overview_task = birdeye.get_token_overview(token_address) |
| #1152 | |
| #1153 | security, creation, overview = await asyncio.gather( |
| #1154 | security_task, |
| #1155 | creation_task, |
| #1156 | overview_task, |
| #1157 | return_exceptions=True |
| #1158 | ) |
| #1159 | |
| #1160 | # Process security data |
| #1161 | security_info = {} |
| #1162 | if not isinstance(security, Exception) and security: |
| #1163 | security_info = { |
| #1164 | "freeze_authority": security.get("freezeAuthority"), |
| #1165 | "mint_authority": security.get("mintAuthority"), |
| #1166 | "is_mutable": security.get("isMutable"), |
| #1167 | "top_holders": security.get("top10HolderPercent"), |
| #1168 | "creator_balance_percent": security.get("creatorBalance"), |
| #1169 | "risks": [] |
| #1170 | } |
| #1171 | |
| #1172 | # Identify risks |
| #1173 | if security.get("freezeAuthority"): |
| #1174 | security_info["risks"].append("Has freeze authority - tokens can be frozen") |
| #1175 | if security.get("mintAuthority"): |
| #1176 | security_info["risks"].append("Has mint authority - supply can be inflated") |
| #1177 | if security.get("isMutable"): |
| #1178 | security_info["risks"].append("Mutable metadata - can be changed") |
| #1179 | top_holder_pct = float(security.get("top10HolderPercent", 0)) |
| #1180 | if top_holder_pct > 50: |
| #1181 | security_info["risks"].append(f"High concentration: Top 10 holders own {top_holder_pct:.1f}%") |
| #1182 | |
| #1183 | # Process creation data |
| #1184 | creation_info = {} |
| #1185 | if not isinstance(creation, Exception) and creation: |
| #1186 | creation_info = { |
| #1187 | "creator": creation.get("creator"), |
| #1188 | "created_at": creation.get("creationTimestamp"), |
| #1189 | "creation_tx": creation.get("creationTx"), |
| #1190 | } |
| #1191 | |
| #1192 | # Process overview data |
| #1193 | market_data = {} |
| #1194 | if not isinstance(overview, Exception): |
| #1195 | market_data = { |
| #1196 | "symbol": overview.symbol, |
| #1197 | "name": overview.name, |
| #1198 | "price_usd": overview.price, |
| #1199 | "price_change_24h_percent": overview.price_change_24h, |
| #1200 | "volume_24h_usd": overview.volume_24h, |
| #1201 | "liquidity_usd": overview.liquidity, |
| #1202 | "market_cap_usd": overview.market_cap, |
| #1203 | "holder_count": overview.holder_count, |
| #1204 | "supply": overview.supply, |
| #1205 | } |
| #1206 | |
| #1207 | result = { |
| #1208 | "token_address": token_address, |
| #1209 | "security_analysis": security_info, |
| #1210 | "creation_info": creation_info, |
| #1211 | "market_data": market_data, |
| #1212 | "risk_level": "HIGH" if len(security_info.get("risks", [])) >= 2 else |
| #1213 | "MEDIUM" if len(security_info.get("risks", [])) == 1 else |
| #1214 | "LOW", |
| #1215 | "recommendation": "⚠️ CAUTION" if len(security_info.get("risks", [])) >= 2 else |
| #1216 | "✓ Looks safer" if len(security_info.get("risks", [])) == 0 else |
| #1217 | "Research more" |
| #1218 | } |
| #1219 | |
| #1220 | return ToolResult( |
| #1221 | success=True, |
| #1222 | content=json.dumps(result, indent=2) |
| #1223 | ) |
| #1224 | except Exception as e: |
| #1225 | return ToolResult(success=False, error=f"Failed to analyze token: {str(e)}") |
| #1226 | |
| #1227 | |
| #1228 | class PostToTwitterTool(Tool): |
| #1229 | """Tool to post tweets to Twitter/X.""" |
| #1230 | |
| #1231 | @property |
| #1232 | def name(self) -> str: |
| #1233 | return "post_to_twitter" |
| #1234 | |
| #1235 | @property |
| #1236 | def description(self) -> str: |
| #1237 | return """Post a tweet to Twitter/X. Use this to share trading updates, alerts, portfolio performance, |
| #1238 | token discoveries, or any other information. Maximum 280 characters. |
| #1239 | Examples: |
| #1240 | - "Just bought 100 SOL of $BONK at $0.000015! 🚀" |
| #1241 | - "My portfolio is up 25% today thanks to $WIF and $BONK! 📈" |
| #1242 | - "Found a gem: $XYZ has 10x potential, low mcap, strong community 💎" |
| #1243 | """ |
| #1244 | |
| #1245 | @property |
| #1246 | def parameters(self) -> dict[str, Any]: |
| #1247 | return { |
| #1248 | "type": "object", |
| #1249 | "properties": { |
| #1250 | "text": { |
| #1251 | "type": "string", |
| #1252 | "description": "The tweet text to post. Maximum 280 characters.", |
| #1253 | }, |
| #1254 | "reply_to_tweet_id": { |
| #1255 | "type": "string", |
| #1256 | "description": "Optional: Tweet ID to reply to", |
| #1257 | }, |
| #1258 | }, |
| #1259 | "required": ["text"], |
| #1260 | } |
| #1261 | |
| #1262 | async def execute(self, text: str, reply_to_tweet_id: str = None) -> ToolResult: |
| #1263 | try: |
| #1264 | twitter = get_twitter_client() |
| #1265 | |
| #1266 | # Validate text length |
| #1267 | if len(text) > 280: |
| #1268 | return ToolResult( |
| #1269 | success=False, |
| #1270 | error=f"Tweet too long: {len(text)} characters (max 280). Please shorten the message." |
| #1271 | ) |
| #1272 | |
| #1273 | # Post the tweet |
| #1274 | result = await twitter.post_tweet( |
| #1275 | text=text, |
| #1276 | reply_to_tweet_id=reply_to_tweet_id, |
| #1277 | ) |
| #1278 | |
| #1279 | if result.success: |
| #1280 | return ToolResult( |
| #1281 | success=True, |
| #1282 | content=json.dumps({ |
| #1283 | "tweet_id": result.tweet_id, |
| #1284 | "text": result.text, |
| #1285 | "url": result.url, |
| #1286 | "message": f"Successfully posted tweet! View it at: {result.url}" |
| #1287 | }, indent=2) |
| #1288 | ) |
| #1289 | else: |
| #1290 | return ToolResult(success=False, error=result.error) |
| #1291 | |
| #1292 | except Exception as e: |
| #1293 | return ToolResult(success=False, error=f"Failed to post tweet: {str(e)}") |
| #1294 | |
| #1295 | |
| #1296 | class GenerateImageTool(Tool): |
| #1297 | """Tool to generate images using MiniMax AI.""" |
| #1298 | |
| #1299 | @property |
| #1300 | def name(self) -> str: |
| #1301 | return "generate_image" |
| #1302 | |
| #1303 | @property |
| #1304 | def description(self) -> str: |
| #1305 | return """Generate AI images from text descriptions using MiniMax. |
| #1306 | Create memes, logos, charts, NFT art, or any visual content. |
| #1307 | Specify the desired image style, subject, colors, and aspect ratio. |
| #1308 | Returns a URL to download the generated image. |
| #1309 | Examples: |
| #1310 | - "Generate a meme about Solana being fast" |
| #1311 | - "Create a logo for a DeFi protocol" |
| #1312 | - "Make a chart showing profits going up" |
| #1313 | """ |
| #1314 | |
| #1315 | @property |
| #1316 | def parameters(self) -> dict[str, Any]: |
| #1317 | return { |
| #1318 | "type": "object", |
| #1319 | "properties": { |
| #1320 | "prompt": { |
| #1321 | "type": "string", |
| #1322 | "description": "Detailed description of the image to generate", |
| #1323 | }, |
| #1324 | "ratio": { |
| #1325 | "type": "string", |
| #1326 | "description": "Aspect ratio: '1:1' (square), '3:4' (portrait), '4:3' (landscape), '16:9' (widescreen). Default: '1:1'", |
| #1327 | "enum": ["1:1", "3:4", "4:3", "16:9"], |
| #1328 | }, |
| #1329 | "num_images": { |
| #1330 | "type": "integer", |
| #1331 | "description": "Number of images to generate (1-4). Default: 1", |
| #1332 | }, |
| #1333 | }, |
| #1334 | "required": ["prompt"], |
| #1335 | } |
| #1336 | |
| #1337 | async def execute(self, prompt: str, ratio: str = "1:1", num_images: int = 1) -> ToolResult: |
| #1338 | try: |
| #1339 | minimax = get_minimax_client() |
| #1340 | |
| #1341 | # Map string ratio to enum |
| #1342 | from clients.minimax_client import ImageRatio |
| #1343 | ratio_map = { |
| #1344 | "1:1": ImageRatio.SQUARE, |
| #1345 | "3:4": ImageRatio.PORTRAIT, |
| #1346 | "4:3": ImageRatio.LANDSCAPE, |
| #1347 | "16:9": ImageRatio.WIDESCREEN, |
| #1348 | } |
| #1349 | ratio_enum = ratio_map.get(ratio, ImageRatio.SQUARE) |
| #1350 | |
| #1351 | # Generate image |
| #1352 | response = await minimax.generate_image( |
| #1353 | prompt=prompt, |
| #1354 | ratio=ratio_enum, |
| #1355 | num_images=num_images |
| #1356 | ) |
| #1357 | |
| #1358 | # Extract image URLs |
| #1359 | images = response.get("data", {}).get("images", []) |
| #1360 | |
| #1361 | if not images: |
| #1362 | return ToolResult( |
| #1363 | success=False, |
| #1364 | error="No images generated" |
| #1365 | ) |
| #1366 | |
| #1367 | result = { |
| #1368 | "prompt": prompt, |
| #1369 | "ratio": ratio, |
| #1370 | "images_generated": len(images), |
| #1371 | "images": [ |
| #1372 | { |
| #1373 | "url": img.get("url"), |
| #1374 | "file_id": img.get("file_id") |
| #1375 | } |
| #1376 | for img in images |
| #1377 | ] |
| #1378 | } |
| #1379 | |
| #1380 | return ToolResult( |
| #1381 | success=True, |
| #1382 | content=json.dumps(result, indent=2) |
| #1383 | ) |
| #1384 | |
| #1385 | except Exception as e: |
| #1386 | return ToolResult(success=False, error=f"Failed to generate image: {str(e)}") |
| #1387 | |
| #1388 | |
| #1389 | class GenerateMusicTool(Tool): |
| #1390 | """Tool to generate music using MiniMax AI.""" |
| #1391 | |
| #1392 | @property |
| #1393 | def name(self) -> str: |
| #1394 | return "generate_music" |
| #1395 | |
| #1396 | @property |
| #1397 | def description(self) -> str: |
| #1398 | return """Generate AI music from text descriptions using MiniMax. |
| #1399 | Create background music, theme songs, or audio content. |
| #1400 | Can generate instrumental tracks or music with lyrics. |
| #1401 | Examples: |
| #1402 | - "Create upbeat electronic music for a trading video" |
| #1403 | - "Generate chill lo-fi beats" |
| #1404 | - "Make a hype song about Solana with lyrics: 'Solana to the moon...'" |
| #1405 | """ |
| #1406 | |
| #1407 | @property |
| #1408 | def parameters(self) -> dict[str, Any]: |
| #1409 | return { |
| #1410 | "type": "object", |
| #1411 | "properties": { |
| #1412 | "prompt": { |
| #1413 | "type": "string", |
| #1414 | "description": "Description of the music style, mood, genre to generate", |
| #1415 | }, |
| #1416 | "lyrics": { |
| #1417 | "type": "string", |
| #1418 | "description": "Optional lyrics to sing in the music", |
| #1419 | }, |
| #1420 | "duration": { |
| #1421 | "type": "integer", |
| #1422 | "description": "Duration in seconds (max 120). Default: 30", |
| #1423 | }, |
| #1424 | "instrumental": { |
| #1425 | "type": "boolean", |
| #1426 | "description": "Generate instrumental only (no vocals). Default: false", |
| #1427 | }, |
| #1428 | }, |
| #1429 | "required": ["prompt"], |
| #1430 | } |
| #1431 | |
| #1432 | async def execute( |
| #1433 | self, |
| #1434 | prompt: str, |
| #1435 | lyrics: str = None, |
| #1436 | duration: int = 30, |
| #1437 | instrumental: bool = False |
| #1438 | ) -> ToolResult: |
| #1439 | try: |
| #1440 | minimax = get_minimax_client() |
| #1441 | |
| #1442 | # Generate music |
| #1443 | response = await minimax.generate_music( |
| #1444 | prompt=prompt, |
| #1445 | lyrics=lyrics, |
| #1446 | duration=duration, |
| #1447 | instrumental=instrumental |
| #1448 | ) |
| #1449 | |
| #1450 | # Extract audio info |
| #1451 | audio_url = response.get("data", {}).get("audio_url") |
| #1452 | file_id = response.get("data", {}).get("file_id") |
| #1453 | |
| #1454 | if not audio_url: |
| #1455 | return ToolResult( |
| #1456 | success=False, |
| #1457 | error="No audio generated" |
| #1458 | ) |
| #1459 | |
| #1460 | result = { |
| #1461 | "prompt": prompt, |
| #1462 | "lyrics": lyrics if lyrics else "(instrumental)", |
| #1463 | "duration": duration, |
| #1464 | "audio_url": audio_url, |
| #1465 | "file_id": file_id, |
| #1466 | "message": f"Successfully generated {duration}s music track!" |
| #1467 | } |
| #1468 | |
| #1469 | return ToolResult( |
| #1470 | success=True, |
| #1471 | content=json.dumps(result, indent=2) |
| #1472 | ) |
| #1473 | |
| #1474 | except Exception as e: |
| #1475 | return ToolResult(success=False, error=f"Failed to generate music: {str(e)}") |
| #1476 | |
| #1477 | |
| #1478 | class GenerateVideoTool(Tool): |
| #1479 | """Tool to generate videos using MiniMax AI.""" |
| #1480 | |
| #1481 | @property |
| #1482 | def name(self) -> str: |
| #1483 | return "generate_video" |
| #1484 | |
| #1485 | @property |
| #1486 | def description(self) -> str: |
| #1487 | return """Generate AI videos from text descriptions using MiniMax. |
| #1488 | Create promotional videos, explainers, animations, or video memes. |
| #1489 | Can optionally specify first and last frame images. |
| #1490 | Note: Video generation takes time (30s-5min). The tool will wait for completion. |
| #1491 | Examples: |
| #1492 | - "Create a video of a rocket launching to represent token price going up" |
| #1493 | - "Generate an animation showing money flowing into a wallet" |
| #1494 | - "Make a video meme about diamond hands holding through a dip" |
| #1495 | """ |
| #1496 | |
| #1497 | @property |
| #1498 | def parameters(self) -> dict[str, Any]: |
| #1499 | return { |
| #1500 | "type": "object", |
| #1501 | "properties": { |
| #1502 | "prompt": { |
| #1503 | "type": "string", |
| #1504 | "description": "Detailed description of the video to generate", |
| #1505 | }, |
| #1506 | "ratio": { |
| #1507 | "type": "string", |
| #1508 | "description": "Aspect ratio: '1:1' (square), '9:16' (portrait/mobile), '16:9' (landscape). Default: '16:9'", |
| #1509 | "enum": ["1:1", "9:16", "16:9"], |
| #1510 | }, |
| #1511 | "duration": { |
| #1512 | "type": "integer", |
| #1513 | "description": "Duration in seconds (2-6). Default: 5", |
| #1514 | }, |
| #1515 | }, |
| #1516 | "required": ["prompt"], |
| #1517 | } |
| #1518 | |
| #1519 | async def execute( |
| #1520 | self, |
| #1521 | prompt: str, |
| #1522 | ratio: str = "16:9", |
| #1523 | duration: int = 5 |
| #1524 | ) -> ToolResult: |
| #1525 | try: |
| #1526 | minimax = get_minimax_client() |
| #1527 | |
| #1528 | # Map string ratio to enum |
| #1529 | from clients.minimax_client import VideoRatio |
| #1530 | ratio_map = { |
| #1531 | "1:1": VideoRatio.SQUARE, |
| #1532 | "9:16": VideoRatio.PORTRAIT, |
| #1533 | "16:9": VideoRatio.LANDSCAPE, |
| #1534 | } |
| #1535 | ratio_enum = ratio_map.get(ratio, VideoRatio.LANDSCAPE) |
| #1536 | |
| #1537 | # Generate video (this will wait for completion) |
| #1538 | result = await minimax.generate_video( |
| #1539 | prompt=prompt, |
| #1540 | ratio=ratio_enum, |
| #1541 | duration=duration, |
| #1542 | wait_for_completion=True, |
| #1543 | max_wait=300 # 5 minutes max |
| #1544 | ) |
| #1545 | |
| #1546 | if result.status == "success": |
| #1547 | return ToolResult( |
| #1548 | success=True, |
| #1549 | content=json.dumps({ |
| #1550 | "prompt": prompt, |
| #1551 | "ratio": ratio, |
| #1552 | "duration": duration, |
| #1553 | "task_id": result.task_id, |
| #1554 | "file_id": result.file_id, |
| #1555 | "download_url": result.download_url, |
| #1556 | "message": "Video generated successfully! Download at the URL provided." |
| #1557 | }, indent=2) |
| #1558 | ) |
| #1559 | elif result.status == "timeout": |
| #1560 | return ToolResult( |
| #1561 | success=False, |
| #1562 | error=f"Video generation timed out. Task ID: {result.task_id}. Try again later or check status." |
| #1563 | ) |
| #1564 | else: |
| #1565 | return ToolResult( |
| #1566 | success=False, |
| #1567 | error=f"Video generation failed: {result.error}" |
| #1568 | ) |
| #1569 | |
| #1570 | except Exception as e: |
| #1571 | return ToolResult(success=False, error=f"Failed to generate video: {str(e)}") |
| #1572 | |
| #1573 | |
| #1574 | class TextToSpeechTool(Tool): |
| #1575 | """Tool to convert text to speech using MiniMax AI.""" |
| #1576 | |
| #1577 | @property |
| #1578 | def name(self) -> str: |
| #1579 | return "text_to_speech" |
| #1580 | |
| #1581 | @property |
| #1582 | def description(self) -> str: |
| #1583 | return """Convert text to natural-sounding speech using MiniMax AI. |
| #1584 | Create voiceovers for videos, audio announcements, or narration. |
| #1585 | Adjust speed and pitch for different effects. |
| #1586 | Examples: |
| #1587 | - "Convert this to speech: 'Welcome to MAWD Trading Bot!'" |
| #1588 | - "Create audio announcement: 'SOL just hit $200!'" |
| #1589 | - "Make voiceover: 'Here are today's top performing tokens...'" |
| #1590 | """ |
| #1591 | |
| #1592 | @property |
| #1593 | def parameters(self) -> dict[str, Any]: |
| #1594 | return { |
| #1595 | "type": "object", |
| #1596 | "properties": { |
| #1597 | "text": { |
| #1598 | "type": "string", |
| #1599 | "description": "The text to convert to speech", |
| #1600 | }, |
| #1601 | "speed": { |
| #1602 | "type": "number", |
| #1603 | "description": "Speech speed (0.5-2.0). 1.0 is normal. Default: 1.0", |
| #1604 | }, |
| #1605 | "pitch": { |
| #1606 | "type": "number", |
| #1607 | "description": "Voice pitch (0.5-2.0). 1.0 is normal. Default: 1.0", |
| #1608 | }, |
| #1609 | }, |
| #1610 | "required": ["text"], |
| #1611 | } |
| #1612 | |
| #1613 | async def execute( |
| #1614 | self, |
| #1615 | text: str, |
| #1616 | speed: float = 1.0, |
| #1617 | pitch: float = 1.0 |
| #1618 | ) -> ToolResult: |
| #1619 | try: |
| #1620 | minimax = get_minimax_client() |
| #1621 | |
| #1622 | # Generate speech |
| #1623 | response = await minimax.text_to_speech( |
| #1624 | text=text, |
| #1625 | speed=speed, |
| #1626 | pitch=pitch |
| #1627 | ) |
| #1628 | |
| #1629 | # Extract audio info |
| #1630 | audio_url = response.get("data", {}).get("audio_url") |
| #1631 | file_id = response.get("data", {}).get("file_id") |
| #1632 | |
| #1633 | if not audio_url: |
| #1634 | return ToolResult( |
| #1635 | success=False, |
| #1636 | error="No audio generated" |
| #1637 | ) |
| #1638 | |
| #1639 | result = { |
| #1640 | "text": text, |
| #1641 | "speed": speed, |
| #1642 | "pitch": pitch, |
| #1643 | "audio_url": audio_url, |
| #1644 | "file_id": file_id, |
| #1645 | "message": "Successfully converted text to speech!" |
| #1646 | } |
| #1647 | |
| #1648 | return ToolResult( |
| #1649 | success=True, |
| #1650 | content=json.dumps(result, indent=2) |
| #1651 | ) |
| #1652 | |
| #1653 | except Exception as e: |
| #1654 | return ToolResult(success=False, error=f"Failed to generate speech: {str(e)}") |
| #1655 | |
| #1656 | |
| #1657 | class WebSearchTool(Tool): |
| #1658 | """Tool for real-time web search using SearchAPI.""" |
| #1659 | |
| #1660 | @property |
| #1661 | def name(self) -> str: |
| #1662 | return "web_search" |
| #1663 | |
| #1664 | @property |
| #1665 | def description(self) -> str: |
| #1666 | return """Search the web in real-time for current information, news, prices, or any topic. |
| #1667 | Returns top search results with snippets, links, and AI overview if available. |
| #1668 | Use this for market research, news, trending topics, or any information not in your knowledge.""" |
| #1669 | |
| #1670 | @property |
| #1671 | def parameters(self) -> dict[str, Any]: |
| #1672 | return { |
| #1673 | "type": "object", |
| #1674 | "properties": { |
| #1675 | "query": { |
| #1676 | "type": "string", |
| #1677 | "description": "The search query. Be specific and use keywords relevant to what you're looking for.", |
| #1678 | }, |
| #1679 | "search_type": { |
| #1680 | "type": "string", |
| #1681 | "enum": ["general", "news"], |
| #1682 | "description": "Type of search. Use 'news' for recent news articles, 'general' for all web results. Default is 'general'.", |
| #1683 | "default": "general", |
| #1684 | }, |
| #1685 | }, |
| #1686 | "required": ["query"], |
| #1687 | } |
| #1688 | |
| #1689 | async def execute(self, query: str, search_type: str = "general") -> ToolResult: |
| #1690 | try: |
| #1691 | search = get_search_client() |
| #1692 | |
| #1693 | if search_type == "news": |
| #1694 | result = await search.search_news(query, time_period="last_day") |
| #1695 | else: |
| #1696 | result = await search.search(query) |
| #1697 | |
| #1698 | # Format results |
| #1699 | output = { |
| #1700 | "query": result.query, |
| #1701 | "total_results": result.total_results, |
| #1702 | "top_results": [ |
| #1703 | { |
| #1704 | "title": r.title, |
| #1705 | "link": r.link, |
| #1706 | "snippet": r.snippet, |
| #1707 | "source": r.source, |
| #1708 | "date": r.date, |
| #1709 | } |
| #1710 | for r in result.results[:5] |
| #1711 | ], |
| #1712 | } |
| #1713 | |
| #1714 | if result.ai_overview: |
| #1715 | output["ai_overview"] = result.ai_overview |
| #1716 | |
| #1717 | if result.answer_box: |
| #1718 | output["answer_box"] = result.answer_box |
| #1719 | |
| #1720 | if result.related_searches: |
| #1721 | output["related_searches"] = result.related_searches |
| #1722 | |
| #1723 | return ToolResult( |
| #1724 | success=True, |
| #1725 | content=json.dumps(output, indent=2) |
| #1726 | ) |
| #1727 | |
| #1728 | except Exception as e: |
| #1729 | return ToolResult(success=False, error=f"Search failed: {str(e)}") |
| #1730 | |
| #1731 | |
| #1732 | class AnalyzeSolanaAddressTool(Tool): |
| #1733 | """Tool for unified Solana blockchain analysis - automatically detects and analyzes contracts, wallets, or transactions.""" |
| #1734 | |
| #1735 | @property |
| #1736 | def name(self) -> str: |
| #1737 | return "analyze_solana_address" |
| #1738 | |
| #1739 | @property |
| #1740 | def description(self) -> str: |
| #1741 | return """Analyze any Solana blockchain address in real-time. Automatically detects whether the address is: |
| #1742 | - Token contract: Returns token info, price, market cap, volume, security analysis, and OHLCV chart data |
| #1743 | - Wallet address: Returns SOL balance, token holdings, NFT holdings, and total assets |
| #1744 | - Transaction signature: Returns transaction details, status, fees, and involved accounts |
| #1745 | |
| #1746 | Just provide the address and get comprehensive real-time blockchain data.""" |
| #1747 | |
| #1748 | @property |
| #1749 | def parameters(self) -> dict[str, Any]: |
| #1750 | return { |
| #1751 | "type": "object", |
| #1752 | "properties": { |
| #1753 | "address": { |
| #1754 | "type": "string", |
| #1755 | "description": "Solana address to analyze. Can be a token contract (32-44 chars), wallet address (32-44 chars), or transaction signature (88 chars).", |
| #1756 | }, |
| #1757 | }, |
| #1758 | "required": ["address"], |
| #1759 | } |
| #1760 | |
| #1761 | async def execute(self, address: str) -> ToolResult: |
| #1762 | try: |
| #1763 | analyzer = get_solana_analyzer() |
| #1764 | |
| #1765 | result = await analyzer.analyze(address) |
| #1766 | |
| #1767 | output = { |
| #1768 | "address": result.address, |
| #1769 | "type": result.address_type.value, |
| #1770 | "data_source": result.source, |
| #1771 | "analysis": result.data, |
| #1772 | } |
| #1773 | |
| #1774 | return ToolResult( |
| #1775 | success=True, |
| #1776 | content=json.dumps(output, indent=2) |
| #1777 | ) |
| #1778 | |
| #1779 | except Exception as e: |
| #1780 | return ToolResult(success=False, error=f"Analysis failed: {str(e)}") |
| #1781 | |
| #1782 | |
| #1783 | # ================== |
| #1784 | # Aster DEX Trading Tools |
| #1785 | # ================== |
| #1786 | |
| #1787 | class AsterOpenLongTool(Tool): |
| #1788 | """Tool to open a long perpetual position on Aster DEX.""" |
| #1789 | |
| #1790 | @property |
| #1791 | def name(self) -> str: |
| #1792 | return "aster_open_long" |
| #1793 | |
| #1794 | @property |
| #1795 | def description(self) -> str: |
| #1796 | return """Open a LONG perpetual position on Aster DEX. |
| #1797 | Go long when you expect the price to increase. |
| #1798 | WARNING: Perpetuals trading involves leverage and high risk. Can result in liquidation.""" |
| #1799 | |
| #1800 | @property |
| #1801 | def parameters(self) -> dict[str, Any]: |
| #1802 | return { |
| #1803 | "type": "object", |
| #1804 | "properties": { |
| #1805 | "symbol": { |
| #1806 | "type": "string", |
| #1807 | "description": "Trading pair (e.g., 'BTCUSDT', 'ETHUSDT', 'SOLUSDT')", |
| #1808 | }, |
| #1809 | "quantity": { |
| #1810 | "type": "number", |
| #1811 | "description": "Position size in base asset", |
| #1812 | }, |
| #1813 | "order_type": { |
| #1814 | "type": "string", |
| #1815 | "enum": ["MARKET", "LIMIT"], |
| #1816 | "description": "Order type. MARKET for immediate execution, LIMIT for specific price.", |
| #1817 | "default": "MARKET", |
| #1818 | }, |
| #1819 | "price": { |
| #1820 | "type": "number", |
| #1821 | "description": "Limit price (required if order_type is LIMIT)", |
| #1822 | }, |
| #1823 | }, |
| #1824 | "required": ["symbol", "quantity"], |
| #1825 | } |
| #1826 | |
| #1827 | async def execute( |
| #1828 | self, |
| #1829 | symbol: str, |
| #1830 | quantity: float, |
| #1831 | order_type: str = "MARKET", |
| #1832 | price: float = None |
| #1833 | ) -> ToolResult: |
| #1834 | try: |
| #1835 | aster = get_aster_client() |
| #1836 | |
| #1837 | result = await aster.place_order( |
| #1838 | symbol=symbol, |
| #1839 | side='BUY', |
| #1840 | order_type=order_type, |
| #1841 | quantity=quantity, |
| #1842 | position_side='LONG', |
| #1843 | price=price, |
| #1844 | ) |
| #1845 | |
| #1846 | return ToolResult( |
| #1847 | success=True, |
| #1848 | content=json.dumps({ |
| #1849 | "status": "success", |
| #1850 | "order_id": result.get("orderId"), |
| #1851 | "symbol": result.get("symbol"), |
| #1852 | "side": "LONG", |
| #1853 | "quantity": result.get("origQty"), |
| #1854 | "price": result.get("price"), |
| #1855 | "type": result.get("type"), |
| #1856 | "message": f"Successfully opened LONG position for {symbol}" |
| #1857 | }, indent=2) |
| #1858 | ) |
| #1859 | except Exception as e: |
| #1860 | return ToolResult(success=False, error=f"Failed to open long: {str(e)}") |
| #1861 | |
| #1862 | |
| #1863 | class AsterOpenShortTool(Tool): |
| #1864 | """Tool to open a short perpetual position on Aster DEX.""" |
| #1865 | |
| #1866 | @property |
| #1867 | def name(self) -> str: |
| #1868 | return "aster_open_short" |
| #1869 | |
| #1870 | @property |
| #1871 | def description(self) -> str: |
| #1872 | return """Open a SHORT perpetual position on Aster DEX. |
| #1873 | Go short when you expect the price to decrease. |
| #1874 | WARNING: Perpetuals trading involves leverage and high risk. Can result in liquidation.""" |
| #1875 | |
| #1876 | @property |
| #1877 | def parameters(self) -> dict[str, Any]: |
| #1878 | return { |
| #1879 | "type": "object", |
| #1880 | "properties": { |
| #1881 | "symbol": { |
| #1882 | "type": "string", |
| #1883 | "description": "Trading pair (e.g., 'BTCUSDT', 'ETHUSDT', 'SOLUSDT')", |
| #1884 | }, |
| #1885 | "quantity": { |
| #1886 | "type": "number", |
| #1887 | "description": "Position size in base asset", |
| #1888 | }, |
| #1889 | "order_type": { |
| #1890 | "type": "string", |
| #1891 | "enum": ["MARKET", "LIMIT"], |
| #1892 | "description": "Order type. MARKET for immediate execution, LIMIT for specific price.", |
| #1893 | "default": "MARKET", |
| #1894 | }, |
| #1895 | "price": { |
| #1896 | "type": "number", |
| #1897 | "description": "Limit price (required if order_type is LIMIT)", |
| #1898 | }, |
| #1899 | }, |
| #1900 | "required": ["symbol", "quantity"], |
| #1901 | } |
| #1902 | |
| #1903 | async def execute( |
| #1904 | self, |
| #1905 | symbol: str, |
| #1906 | quantity: float, |
| #1907 | order_type: str = "MARKET", |
| #1908 | price: float = None |
| #1909 | ) -> ToolResult: |
| #1910 | try: |
| #1911 | aster = get_aster_client() |
| #1912 | |
| #1913 | result = await aster.place_order( |
| #1914 | symbol=symbol, |
| #1915 | side='SELL', |
| #1916 | order_type=order_type, |
| #1917 | quantity=quantity, |
| #1918 | position_side='SHORT', |
| #1919 | price=price, |
| #1920 | ) |
| #1921 | |
| #1922 | return ToolResult( |
| #1923 | success=True, |
| #1924 | content=json.dumps({ |
| #1925 | "status": "success", |
| #1926 | "order_id": result.get("orderId"), |
| #1927 | "symbol": result.get("symbol"), |
| #1928 | "side": "SHORT", |
| #1929 | "quantity": result.get("origQty"), |
| #1930 | "price": result.get("price"), |
| #1931 | "type": result.get("type"), |
| #1932 | "message": f"Successfully opened SHORT position for {symbol}" |
| #1933 | }, indent=2) |
| #1934 | ) |
| #1935 | except Exception as e: |
| #1936 | return ToolResult(success=False, error=f"Failed to open short: {str(e)}") |
| #1937 | |
| #1938 | |
| #1939 | class AsterClosePerpPositionTool(Tool): |
| #1940 | """Tool to close a perpetual position on Aster DEX.""" |
| #1941 | |
| #1942 | @property |
| #1943 | def name(self) -> str: |
| #1944 | return "aster_close_position" |
| #1945 | |
| #1946 | @property |
| #1947 | def description(self) -> str: |
| #1948 | return """Close an open perpetual position on Aster DEX. |
| #1949 | Closes LONG positions by selling, SHORT positions by buying. |
| #1950 | Use close_position=True to close the entire position at market price.""" |
| #1951 | |
| #1952 | @property |
| #1953 | def parameters(self) -> dict[str, Any]: |
| #1954 | return { |
| #1955 | "type": "object", |
| #1956 | "properties": { |
| #1957 | "symbol": { |
| #1958 | "type": "string", |
| #1959 | "description": "Trading pair (e.g., 'BTCUSDT', 'ETHUSDT', 'SOLUSDT')", |
| #1960 | }, |
| #1961 | "position_side": { |
| #1962 | "type": "string", |
| #1963 | "enum": ["LONG", "SHORT"], |
| #1964 | "description": "Which position to close: LONG or SHORT", |
| #1965 | }, |
| #1966 | }, |
| #1967 | "required": ["symbol", "position_side"], |
| #1968 | } |
| #1969 | |
| #1970 | async def execute(self, symbol: str, position_side: str) -> ToolResult: |
| #1971 | try: |
| #1972 | aster = get_aster_client() |
| #1973 | |
| #1974 | # To close a LONG, we SELL. To close a SHORT, we BUY |
| #1975 | side = 'SELL' if position_side == 'LONG' else 'BUY' |
| #1976 | |
| #1977 | result = await aster.place_order( |
| #1978 | symbol=symbol, |
| #1979 | side=side, |
| #1980 | order_type='MARKET', |
| #1981 | quantity=0, # Will be ignored due to close_position=True |
| #1982 | position_side=position_side, |
| #1983 | close_position=True, |
| #1984 | ) |
| #1985 | |
| #1986 | return ToolResult( |
| #1987 | success=True, |
| #1988 | content=json.dumps({ |
| #1989 | "status": "success", |
| #1990 | "order_id": result.get("orderId"), |
| #1991 | "symbol": result.get("symbol"), |
| #1992 | "position_closed": position_side, |
| #1993 | "message": f"Successfully closed {position_side} position for {symbol}" |
| #1994 | }, indent=2) |
| #1995 | ) |
| #1996 | except Exception as e: |
| #1997 | return ToolResult(success=False, error=f"Failed to close position: {str(e)}") |
| #1998 | |
| #1999 | |
| #2000 | class AsterGetPositionsTool(Tool): |
| #2001 | """Tool to get all open perpetual positions on Aster DEX.""" |
| #2002 | |
| #2003 | @property |
| #2004 | def name(self) -> str: |
| #2005 | return "aster_get_positions" |
| #2006 | |
| #2007 | @property |
| #2008 | def description(self) -> str: |
| #2009 | return """Get all open perpetual positions on Aster DEX. |
| #2010 | Shows position side (LONG/SHORT), size, entry price, unrealized PnL, leverage, and margin.""" |
| #2011 | |
| #2012 | @property |
| #2013 | def parameters(self) -> dict[str, Any]: |
| #2014 | return { |
| #2015 | "type": "object", |
| #2016 | "properties": { |
| #2017 | "symbol": { |
| #2018 | "type": "string", |
| #2019 | "description": "Optional: Filter by trading pair (e.g., 'BTCUSDT')", |
| #2020 | }, |
| #2021 | }, |
| #2022 | "required": [], |
| #2023 | } |
| #2024 | |
| #2025 | async def execute(self, symbol: str = None) -> ToolResult: |
| #2026 | try: |
| #2027 | aster = get_aster_client() |
| #2028 | |
| #2029 | positions = await aster.get_positions(symbol=symbol) |
| #2030 | |
| #2031 | # Filter out positions with no size |
| #2032 | active_positions = [ |
| #2033 | { |
| #2034 | "symbol": p.get("symbol"), |
| #2035 | "position_side": p.get("positionSide"), |
| #2036 | "position_amount": float(p.get("positionAmt", 0)), |
| #2037 | "entry_price": float(p.get("entryPrice", 0)), |
| #2038 | "unrealized_profit": float(p.get("unRealizedProfit", 0)), |
| #2039 | "leverage": p.get("leverage"), |
| #2040 | "margin_type": p.get("marginType"), |
| #2041 | "liquidation_price": float(p.get("liquidationPrice", 0)), |
| #2042 | } |
| #2043 | for p in positions |
| #2044 | if float(p.get("positionAmt", 0)) != 0 |
| #2045 | ] |
| #2046 | |
| #2047 | return ToolResult( |
| #2048 | success=True, |
| #2049 | content=json.dumps({ |
| #2050 | "positions_count": len(active_positions), |
| #2051 | "positions": active_positions, |
| #2052 | }, indent=2) |
| #2053 | ) |
| #2054 | except Exception as e: |
| #2055 | return ToolResult(success=False, error=f"Failed to get positions: {str(e)}") |
| #2056 | |
| #2057 | |
| #2058 | class AsterSetLeverageTool(Tool): |
| #2059 | """Tool to set leverage for a trading pair on Aster DEX.""" |
| #2060 | |
| #2061 | @property |
| #2062 | def name(self) -> str: |
| #2063 | return "aster_set_leverage" |
| #2064 | |
| #2065 | @property |
| #2066 | def description(self) -> str: |
| #2067 | return """Set the leverage (1x-125x) for a perpetual trading pair on Aster DEX. |
| #2068 | Higher leverage = higher risk and potential for liquidation. |
| #2069 | Must be set before opening positions.""" |
| #2070 | |
| #2071 | @property |
| #2072 | def parameters(self) -> dict[str, Any]: |
| #2073 | return { |
| #2074 | "type": "object", |
| #2075 | "properties": { |
| #2076 | "symbol": { |
| #2077 | "type": "string", |
| #2078 | "description": "Trading pair (e.g., 'BTCUSDT', 'ETHUSDT', 'SOLUSDT')", |
| #2079 | }, |
| #2080 | "leverage": { |
| #2081 | "type": "integer", |
| #2082 | "description": "Leverage multiplier (1-125). Higher leverage = higher risk.", |
| #2083 | }, |
| #2084 | }, |
| #2085 | "required": ["symbol", "leverage"], |
| #2086 | } |
| #2087 | |
| #2088 | async def execute(self, symbol: str, leverage: int) -> ToolResult: |
| #2089 | try: |
| #2090 | aster = get_aster_client() |
| #2091 | |
| #2092 | result = await aster.change_leverage(symbol=symbol, leverage=leverage) |
| #2093 | |
| #2094 | return ToolResult( |
| #2095 | success=True, |
| #2096 | content=json.dumps({ |
| #2097 | "status": "success", |
| #2098 | "symbol": result.get("symbol"), |
| #2099 | "leverage": result.get("leverage"), |
| #2100 | "message": f"Leverage set to {leverage}x for {symbol}" |
| #2101 | }, indent=2) |
| #2102 | ) |
| #2103 | except Exception as e: |
| #2104 | return ToolResult(success=False, error=f"Failed to set leverage: {str(e)}") |
| #2105 | |
| #2106 | |
| #2107 | class AsterSpotBuyTool(Tool): |
| #2108 | """Tool to buy tokens on Aster DEX spot market.""" |
| #2109 | |
| #2110 | @property |
| #2111 | def name(self) -> str: |
| #2112 | return "aster_spot_buy" |
| #2113 | |
| #2114 | @property |
| #2115 | def description(self) -> str: |
| #2116 | return """Buy tokens on Aster DEX spot market. |
| #2117 | Use MARKET order for immediate execution or LIMIT order to set a specific price. |
| #2118 | For MARKET orders, specify quote_amount (how much USDT to spend). |
| #2119 | For LIMIT orders, specify quantity (how much token to buy) and price.""" |
| #2120 | |
| #2121 | @property |
| #2122 | def parameters(self) -> dict[str, Any]: |
| #2123 | return { |
| #2124 | "type": "object", |
| #2125 | "properties": { |
| #2126 | "symbol": { |
| #2127 | "type": "string", |
| #2128 | "description": "Trading pair (e.g., 'BTCUSDT', 'ETHUSDT', 'SOLUSDT')", |
| #2129 | }, |
| #2130 | "order_type": { |
| #2131 | "type": "string", |
| #2132 | "enum": ["MARKET", "LIMIT"], |
| #2133 | "description": "Order type. MARKET for immediate execution, LIMIT for specific price.", |
| #2134 | "default": "MARKET", |
| #2135 | }, |
| #2136 | "quantity": { |
| #2137 | "type": "number", |
| #2138 | "description": "Amount of base asset to buy (for LIMIT orders)", |
| #2139 | }, |
| #2140 | "quote_amount": { |
| #2141 | "type": "number", |
| #2142 | "description": "Amount of quote asset to spend (for MARKET orders, e.g., USDT amount)", |
| #2143 | }, |
| #2144 | "price": { |
| #2145 | "type": "number", |
| #2146 | "description": "Limit price (required if order_type is LIMIT)", |
| #2147 | }, |
| #2148 | }, |
| #2149 | "required": ["symbol"], |
| #2150 | } |
| #2151 | |
| #2152 | async def execute( |
| #2153 | self, |
| #2154 | symbol: str, |
| #2155 | order_type: str = "MARKET", |
| #2156 | quantity: float = None, |
| #2157 | quote_amount: float = None, |
| #2158 | price: float = None |
| #2159 | ) -> ToolResult: |
| #2160 | try: |
| #2161 | aster = get_aster_client() |
| #2162 | |
| #2163 | result = await aster.spot_place_order( |
| #2164 | symbol=symbol, |
| #2165 | side='BUY', |
| #2166 | order_type=order_type, |
| #2167 | quantity=quantity, |
| #2168 | quote_order_qty=quote_amount, |
| #2169 | price=price, |
| #2170 | ) |
| #2171 | |
| #2172 | return ToolResult( |
| #2173 | success=True, |
| #2174 | content=json.dumps({ |
| #2175 | "status": "success", |
| #2176 | "order_id": result.get("orderId"), |
| #2177 | "symbol": result.get("symbol"), |
| #2178 | "side": "BUY", |
| #2179 | "type": result.get("type"), |
| #2180 | "quantity": result.get("origQty"), |
| #2181 | "price": result.get("price"), |
| #2182 | "filled_quantity": result.get("executedQty"), |
| #2183 | "message": f"Successfully placed spot BUY order for {symbol}" |
| #2184 | }, indent=2) |
| #2185 | ) |
| #2186 | except Exception as e: |
| #2187 | return ToolResult(success=False, error=f"Failed to buy spot: {str(e)}") |
| #2188 | |
| #2189 | |
| #2190 | class AsterSpotSellTool(Tool): |
| #2191 | """Tool to sell tokens on Aster DEX spot market.""" |
| #2192 | |
| #2193 | @property |
| #2194 | def name(self) -> str: |
| #2195 | return "aster_spot_sell" |
| #2196 | |
| #2197 | @property |
| #2198 | def description(self) -> str: |
| #2199 | return """Sell tokens on Aster DEX spot market. |
| #2200 | Use MARKET order for immediate execution or LIMIT order to set a specific price. |
| #2201 | Specify quantity (how much token to sell) for both order types.""" |
| #2202 | |
| #2203 | @property |
| #2204 | def parameters(self) -> dict[str, Any]: |
| #2205 | return { |
| #2206 | "type": "object", |
| #2207 | "properties": { |
| #2208 | "symbol": { |
| #2209 | "type": "string", |
| #2210 | "description": "Trading pair (e.g., 'BTCUSDT', 'ETHUSDT', 'SOLUSDT')", |
| #2211 | }, |
| #2212 | "quantity": { |
| #2213 | "type": "number", |
| #2214 | "description": "Amount of base asset to sell", |
| #2215 | }, |
| #2216 | "order_type": { |
| #2217 | "type": "string", |
| #2218 | "enum": ["MARKET", "LIMIT"], |
| #2219 | "description": "Order type. MARKET for immediate execution, LIMIT for specific price.", |
| #2220 | "default": "MARKET", |
| #2221 | }, |
| #2222 | "price": { |
| #2223 | "type": "number", |
| #2224 | "description": "Limit price (required if order_type is LIMIT)", |
| #2225 | }, |
| #2226 | }, |
| #2227 | "required": ["symbol", "quantity"], |
| #2228 | } |
| #2229 | |
| #2230 | async def execute( |
| #2231 | self, |
| #2232 | symbol: str, |
| #2233 | quantity: float, |
| #2234 | order_type: str = "MARKET", |
| #2235 | price: float = None |
| #2236 | ) -> ToolResult: |
| #2237 | try: |
| #2238 | aster = get_aster_client() |
| #2239 | |
| #2240 | result = await aster.spot_place_order( |
| #2241 | symbol=symbol, |
| #2242 | side='SELL', |
| #2243 | order_type=order_type, |
| #2244 | quantity=quantity, |
| #2245 | price=price, |
| #2246 | ) |
| #2247 | |
| #2248 | return ToolResult( |
| #2249 | success=True, |
| #2250 | content=json.dumps({ |
| #2251 | "status": "success", |
| #2252 | "order_id": result.get("orderId"), |
| #2253 | "symbol": result.get("symbol"), |
| #2254 | "side": "SELL", |
| #2255 | "type": result.get("type"), |
| #2256 | "quantity": result.get("origQty"), |
| #2257 | "price": result.get("price"), |
| #2258 | "filled_quantity": result.get("executedQty"), |
| #2259 | "message": f"Successfully placed spot SELL order for {symbol}" |
| #2260 | }, indent=2) |
| #2261 | ) |
| #2262 | except Exception as e: |
| #2263 | return ToolResult(success=False, error=f"Failed to sell spot: {str(e)}") |
| #2264 | |
| #2265 | |
| #2266 | class AsterGetBalanceTool(Tool): |
| #2267 | """Tool to get account balance on Aster DEX.""" |
| #2268 | |
| #2269 | @property |
| #2270 | def name(self) -> str: |
| #2271 | return "aster_get_balance" |
| #2272 | |
| #2273 | @property |
| #2274 | def description(self) -> str: |
| #2275 | return """Get account balance for both futures and spot accounts on Aster DEX. |
| #2276 | Shows available balance, wallet balance, unrealized PnL, and margin balance.""" |
| #2277 | |
| #2278 | @property |
| #2279 | def parameters(self) -> dict[str, Any]: |
| #2280 | return { |
| #2281 | "type": "object", |
| #2282 | "properties": {}, |
| #2283 | "required": [], |
| #2284 | } |
| #2285 | |
| #2286 | async def execute(self) -> ToolResult: |
| #2287 | try: |
| #2288 | aster = get_aster_client() |
| #2289 | |
| #2290 | # Get futures balance |
| #2291 | futures_balance = await aster.get_account_balance() |
| #2292 | |
| #2293 | # Get spot balance |
| #2294 | try: |
| #2295 | spot_account = await aster.spot_get_account() |
| #2296 | spot_balances = spot_account.get("balances", []) |
| #2297 | except: |
| #2298 | spot_balances = [] |
| #2299 | |
| #2300 | result = { |
| #2301 | "futures_balances": [ |
| #2302 | { |
| #2303 | "asset": b.get("asset"), |
| #2304 | "wallet_balance": float(b.get("walletBalance", 0)), |
| #2305 | "available_balance": float(b.get("availableBalance", 0)), |
| #2306 | "unrealized_profit": float(b.get("unrealizedProfit", 0)), |
| #2307 | "margin_balance": float(b.get("marginBalance", 0)), |
| #2308 | } |
| #2309 | for b in futures_balance |
| #2310 | if float(b.get("walletBalance", 0)) > 0 |
| #2311 | ], |
| #2312 | "spot_balances": [ |
| #2313 | { |
| #2314 | "asset": b.get("asset"), |
| #2315 | "free": float(b.get("free", 0)), |
| #2316 | "locked": float(b.get("locked", 0)), |
| #2317 | "total": float(b.get("free", 0)) + float(b.get("locked", 0)), |
| #2318 | } |
| #2319 | for b in spot_balances |
| #2320 | if float(b.get("free", 0)) > 0 or float(b.get("locked", 0)) > 0 |
| #2321 | ], |
| #2322 | } |
| #2323 | |
| #2324 | return ToolResult( |
| #2325 | success=True, |
| #2326 | content=json.dumps(result, indent=2) |
| #2327 | ) |
| #2328 | except Exception as e: |
| #2329 | return ToolResult(success=False, error=f"Failed to get balance: {str(e)}") |
| #2330 | |
| #2331 | |
| #2332 | class AsterTransferTool(Tool): |
| #2333 | """Tool to transfer funds between futures and spot accounts on Aster DEX.""" |
| #2334 | |
| #2335 | @property |
| #2336 | def name(self) -> str: |
| #2337 | return "aster_transfer" |
| #2338 | |
| #2339 | @property |
| #2340 | def description(self) -> str: |
| #2341 | return """Transfer funds between futures and spot accounts on Aster DEX. |
| #2342 | Type 1: Spot to Futures |
| #2343 | Type 2: Futures to Spot""" |
| #2344 | |
| #2345 | @property |
| #2346 | def parameters(self) -> dict[str, Any]: |
| #2347 | return { |
| #2348 | "type": "object", |
| #2349 | "properties": { |
| #2350 | "asset": { |
| #2351 | "type": "string", |
| #2352 | "description": "Asset to transfer (e.g., 'USDT', 'BTC', 'ETH')", |
| #2353 | }, |
| #2354 | "amount": { |
| #2355 | "type": "number", |
| #2356 | "description": "Amount to transfer", |
| #2357 | }, |
| #2358 | "transfer_type": { |
| #2359 | "type": "integer", |
| #2360 | "enum": [1, 2], |
| #2361 | "description": "Transfer type: 1 = Spot to Futures, 2 = Futures to Spot", |
| #2362 | }, |
| #2363 | }, |
| #2364 | "required": ["asset", "amount", "transfer_type"], |
| #2365 | } |
| #2366 | |
| #2367 | async def execute(self, asset: str, amount: float, transfer_type: int) -> ToolResult: |
| #2368 | try: |
| #2369 | aster = get_aster_client() |
| #2370 | |
| #2371 | result = await aster.transfer_between_futures_spot( |
| #2372 | asset=asset, |
| #2373 | amount=amount, |
| #2374 | transfer_type=transfer_type, |
| #2375 | ) |
| #2376 | |
| #2377 | direction = "Spot → Futures" if transfer_type == 1 else "Futures → Spot" |
| #2378 | |
| #2379 | return ToolResult( |
| #2380 | success=True, |
| #2381 | content=json.dumps({ |
| #2382 | "status": "success", |
| #2383 | "transaction_id": result.get("tranId"), |
| #2384 | "asset": asset, |
| #2385 | "amount": amount, |
| #2386 | "direction": direction, |
| #2387 | "message": f"Successfully transferred {amount} {asset} from {direction}" |
| #2388 | }, indent=2) |
| #2389 | ) |
| #2390 | except Exception as e: |
| #2391 | return ToolResult(success=False, error=f"Failed to transfer: {str(e)}") |
| #2392 | |
| #2393 | |
| #2394 | class AsterGetPriceTool(Tool): |
| #2395 | """Tool to get current price and 24h statistics for a trading pair on Aster DEX.""" |
| #2396 | |
| #2397 | @property |
| #2398 | def name(self) -> str: |
| #2399 | return "aster_get_price" |
| #2400 | |
| #2401 | @property |
| #2402 | def description(self) -> str: |
| #2403 | return """Get current price, 24h change, volume, and trading statistics for any trading pair on Aster DEX. |
| #2404 | Shows price, price change percentage, high, low, volume, and quote volume.""" |
| #2405 | |
| #2406 | @property |
| #2407 | def parameters(self) -> dict[str, Any]: |
| #2408 | return { |
| #2409 | "type": "object", |
| #2410 | "properties": { |
| #2411 | "symbol": { |
| #2412 | "type": "string", |
| #2413 | "description": "Trading pair (e.g., 'BTCUSDT', 'ETHUSDT', 'SOLUSDT')", |
| #2414 | }, |
| #2415 | }, |
| #2416 | "required": ["symbol"], |
| #2417 | } |
| #2418 | |
| #2419 | async def execute(self, symbol: str) -> ToolResult: |
| #2420 | try: |
| #2421 | aster = get_aster_client() |
| #2422 | |
| #2423 | ticker = await aster.get_ticker_24h(symbol=symbol) |
| #2424 | |
| #2425 | result = { |
| #2426 | "symbol": ticker.get("symbol"), |
| #2427 | "last_price": float(ticker.get("lastPrice", 0)), |
| #2428 | "price_change_24h": float(ticker.get("priceChange", 0)), |
| #2429 | "price_change_percent_24h": float(ticker.get("priceChangePercent", 0)), |
| #2430 | "high_24h": float(ticker.get("highPrice", 0)), |
| #2431 | "low_24h": float(ticker.get("lowPrice", 0)), |
| #2432 | "volume_24h": float(ticker.get("volume", 0)), |
| #2433 | "quote_volume_24h": float(ticker.get("quoteVolume", 0)), |
| #2434 | "open_price": float(ticker.get("openPrice", 0)), |
| #2435 | } |
| #2436 | |
| #2437 | return ToolResult( |
| #2438 | success=True, |
| #2439 | content=json.dumps(result, indent=2) |
| #2440 | ) |
| #2441 | except Exception as e: |
| #2442 | return ToolResult(success=False, error=f"Failed to get price: {str(e)}") |
| #2443 | |
| #2444 | |
| #2445 | # ================== |
| #2446 | # Hyperliquid DEX Trading Tools |
| #2447 | # ================== |
| #2448 | |
| #2449 | class HyperliquidGetAccountTool(Tool): |
| #2450 | """Tool to get Hyperliquid account information including balance and positions.""" |
| #2451 | |
| #2452 | @property |
| #2453 | def name(self) -> str: |
| #2454 | return "hyperliquid_get_account" |
| #2455 | |
| #2456 | @property |
| #2457 | def description(self) -> str: |
| #2458 | return """Get Hyperliquid account information including account value, margin summary, withdrawable balance, and all open positions. |
| #2459 | Provides a complete overview of your Hyperliquid perpetuals trading account.""" |
| #2460 | |
| #2461 | @property |
| #2462 | def parameters(self) -> dict[str, Any]: |
| #2463 | return { |
| #2464 | "type": "object", |
| #2465 | "properties": {}, |
| #2466 | "required": [], |
| #2467 | } |
| #2468 | |
| #2469 | async def execute(self) -> ToolResult: |
| #2470 | try: |
| #2471 | hl = get_hyperliquid_client() |
| #2472 | |
| #2473 | # Get account state |
| #2474 | margin_summary = hl.get_margin_summary() |
| #2475 | account_value = hl.get_account_value() |
| #2476 | withdrawable = hl.get_withdrawable() |
| #2477 | positions = hl.get_positions() |
| #2478 | |
| #2479 | # Format positions |
| #2480 | position_list = [ |
| #2481 | { |
| #2482 | "coin": p.coin, |
| #2483 | "size": p.size, |
| #2484 | "entry_price": p.entry_price, |
| #2485 | "position_value": p.position_value, |
| #2486 | "unrealized_pnl": p.unrealized_pnl, |
| #2487 | "return_on_equity": p.return_on_equity, |
| #2488 | "leverage_type": p.leverage_type, |
| #2489 | "leverage_value": p.leverage_value, |
| #2490 | "liquidation_price": p.liquidation_price, |
| #2491 | "margin_used": p.margin_used, |
| #2492 | } |
| #2493 | for p in positions |
| #2494 | ] |
| #2495 | |
| #2496 | result = { |
| #2497 | "account_value_usd": account_value, |
| #2498 | "withdrawable_usd": withdrawable, |
| #2499 | "margin_summary": { |
| #2500 | "account_value": float(margin_summary.get("accountValue", 0)), |
| #2501 | "total_margin_used": float(margin_summary.get("totalMarginUsed", 0)), |
| #2502 | "total_ntl_pos": float(margin_summary.get("totalNtlPos", 0)), |
| #2503 | "total_raw_usd": float(margin_summary.get("totalRawUsd", 0)), |
| #2504 | }, |
| #2505 | "positions_count": len(position_list), |
| #2506 | "positions": position_list, |
| #2507 | } |
| #2508 | |
| #2509 | return ToolResult( |
| #2510 | success=True, |
| #2511 | content=json.dumps(result, indent=2) |
| #2512 | ) |
| #2513 | except Exception as e: |
| #2514 | return ToolResult(success=False, error=f"Failed to get account: {str(e)}") |
| #2515 | |
| #2516 | |
| #2517 | class HyperliquidGetPriceTool(Tool): |
| #2518 | """Tool to get current price for assets on Hyperliquid.""" |
| #2519 | |
| #2520 | @property |
| #2521 | def name(self) -> str: |
| #2522 | return "hyperliquid_get_price" |
| #2523 | |
| #2524 | @property |
| #2525 | def description(self) -> str: |
| #2526 | return """Get current mid prices for assets on Hyperliquid. |
| #2527 | If coin is specified, returns price for that coin. Otherwise returns all prices. |
| #2528 | Common coins: BTC, ETH, SOL, DOGE, WIF, PEPE, ARB, OP, SUI, SEI, TIA, etc.""" |
| #2529 | |
| #2530 | @property |
| #2531 | def parameters(self) -> dict[str, Any]: |
| #2532 | return { |
| #2533 | "type": "object", |
| #2534 | "properties": { |
| #2535 | "coin": { |
| #2536 | "type": "string", |
| #2537 | "description": "Optional: Specific coin to get price for (e.g., 'BTC', 'ETH', 'SOL')", |
| #2538 | }, |
| #2539 | }, |
| #2540 | "required": [], |
| #2541 | } |
| #2542 | |
| #2543 | async def execute(self, coin: str = None) -> ToolResult: |
| #2544 | try: |
| #2545 | hl = get_hyperliquid_client() |
| #2546 | |
| #2547 | if coin: |
| #2548 | price = hl.get_price(coin) |
| #2549 | result = { |
| #2550 | "coin": coin, |
| #2551 | "price_usd": price, |
| #2552 | } |
| #2553 | else: |
| #2554 | all_mids = hl.get_all_mids() |
| #2555 | result = { |
| #2556 | "prices": all_mids, |
| #2557 | "count": len(all_mids), |
| #2558 | } |
| #2559 | |
| #2560 | return ToolResult( |
| #2561 | success=True, |
| #2562 | content=json.dumps(result, indent=2) |
| #2563 | ) |
| #2564 | except Exception as e: |
| #2565 | return ToolResult(success=False, error=f"Failed to get price: {str(e)}") |
| #2566 | |
| #2567 | |
| #2568 | class HyperliquidOpenLongTool(Tool): |
| #2569 | """Tool to open a long perpetual position on Hyperliquid.""" |
| #2570 | |
| #2571 | @property |
| #2572 | def name(self) -> str: |
| #2573 | return "hyperliquid_open_long" |
| #2574 | |
| #2575 | @property |
| #2576 | def description(self) -> str: |
| #2577 | return """Open a LONG perpetual position on Hyperliquid DEX. |
| #2578 | Go long when you expect the price to increase. |
| #2579 | Uses market order with configurable slippage. |
| #2580 | WARNING: Perpetuals trading involves leverage and high risk. Can result in liquidation. |
| #2581 | Common coins: BTC, ETH, SOL, DOGE, WIF, PEPE, ARB, OP, SUI, SEI, TIA, etc.""" |
| #2582 | |
| #2583 | @property |
| #2584 | def parameters(self) -> dict[str, Any]: |
| #2585 | return { |
| #2586 | "type": "object", |
| #2587 | "properties": { |
| #2588 | "coin": { |
| #2589 | "type": "string", |
| #2590 | "description": "Coin to trade (e.g., 'BTC', 'ETH', 'SOL')", |
| #2591 | }, |
| #2592 | "size": { |
| #2593 | "type": "number", |
| #2594 | "description": "Position size in the coin's unit (e.g., 0.1 for 0.1 BTC)", |
| #2595 | }, |
| #2596 | "slippage": { |
| #2597 | "type": "number", |
| #2598 | "description": "Max slippage as decimal (0.05 = 5%). Default: 0.05", |
| #2599 | }, |
| #2600 | }, |
| #2601 | "required": ["coin", "size"], |
| #2602 | } |
| #2603 | |
| #2604 | async def execute(self, coin: str, size: float, slippage: float = 0.05) -> ToolResult: |
| #2605 | try: |
| #2606 | hl = get_hyperliquid_client() |
| #2607 | |
| #2608 | result = hl.market_open(coin=coin, is_buy=True, size=size, slippage=slippage) |
| #2609 | |
| #2610 | if result.get("status") == "ok": |
| #2611 | statuses = result.get("response", {}).get("data", {}).get("statuses", []) |
| #2612 | |
| #2613 | filled_info = [] |
| #2614 | for status in statuses: |
| #2615 | if "filled" in status: |
| #2616 | filled = status["filled"] |
| #2617 | filled_info.append({ |
| #2618 | "order_id": filled.get("oid"), |
| #2619 | "filled_size": filled.get("totalSz"), |
| #2620 | "avg_price": filled.get("avgPx"), |
| #2621 | }) |
| #2622 | elif "error" in status: |
| #2623 | return ToolResult(success=False, error=f"Order error: {status['error']}") |
| #2624 | |
| #2625 | return ToolResult( |
| #2626 | success=True, |
| #2627 | content=json.dumps({ |
| #2628 | "status": "success", |
| #2629 | "coin": coin, |
| #2630 | "side": "LONG", |
| #2631 | "size": size, |
| #2632 | "slippage": slippage, |
| #2633 | "fills": filled_info, |
| #2634 | "message": f"Successfully opened LONG position for {size} {coin}" |
| #2635 | }, indent=2) |
| #2636 | ) |
| #2637 | else: |
| #2638 | return ToolResult(success=False, error=f"Order failed: {result}") |
| #2639 | |
| #2640 | except Exception as e: |
| #2641 | return ToolResult(success=False, error=f"Failed to open long: {str(e)}") |
| #2642 | |
| #2643 | |
| #2644 | class HyperliquidOpenShortTool(Tool): |
| #2645 | """Tool to open a short perpetual position on Hyperliquid.""" |
| #2646 | |
| #2647 | @property |
| #2648 | def name(self) -> str: |
| #2649 | return "hyperliquid_open_short" |
| #2650 | |
| #2651 | @property |
| #2652 | def description(self) -> str: |
| #2653 | return """Open a SHORT perpetual position on Hyperliquid DEX. |
| #2654 | Go short when you expect the price to decrease. |
| #2655 | Uses market order with configurable slippage. |
| #2656 | WARNING: Perpetuals trading involves leverage and high risk. Can result in liquidation. |
| #2657 | Common coins: BTC, ETH, SOL, DOGE, WIF, PEPE, ARB, OP, SUI, SEI, TIA, etc.""" |
| #2658 | |
| #2659 | @property |
| #2660 | def parameters(self) -> dict[str, Any]: |
| #2661 | return { |
| #2662 | "type": "object", |
| #2663 | "properties": { |
| #2664 | "coin": { |
| #2665 | "type": "string", |
| #2666 | "description": "Coin to trade (e.g., 'BTC', 'ETH', 'SOL')", |
| #2667 | }, |
| #2668 | "size": { |
| #2669 | "type": "number", |
| #2670 | "description": "Position size in the coin's unit (e.g., 0.1 for 0.1 BTC)", |
| #2671 | }, |
| #2672 | "slippage": { |
| #2673 | "type": "number", |
| #2674 | "description": "Max slippage as decimal (0.05 = 5%). Default: 0.05", |
| #2675 | }, |
| #2676 | }, |
| #2677 | "required": ["coin", "size"], |
| #2678 | } |
| #2679 | |
| #2680 | async def execute(self, coin: str, size: float, slippage: float = 0.05) -> ToolResult: |
| #2681 | try: |
| #2682 | hl = get_hyperliquid_client() |
| #2683 | |
| #2684 | result = hl.market_open(coin=coin, is_buy=False, size=size, slippage=slippage) |
| #2685 | |
| #2686 | if result.get("status") == "ok": |
| #2687 | statuses = result.get("response", {}).get("data", {}).get("statuses", []) |
| #2688 | |
| #2689 | filled_info = [] |
| #2690 | for status in statuses: |
| #2691 | if "filled" in status: |
| #2692 | filled = status["filled"] |
| #2693 | filled_info.append({ |
| #2694 | "order_id": filled.get("oid"), |
| #2695 | "filled_size": filled.get("totalSz"), |
| #2696 | "avg_price": filled.get("avgPx"), |
| #2697 | }) |
| #2698 | elif "error" in status: |
| #2699 | return ToolResult(success=False, error=f"Order error: {status['error']}") |
| #2700 | |
| #2701 | return ToolResult( |
| #2702 | success=True, |
| #2703 | content=json.dumps({ |
| #2704 | "status": "success", |
| #2705 | "coin": coin, |
| #2706 | "side": "SHORT", |
| #2707 | "size": size, |
| #2708 | "slippage": slippage, |
| #2709 | "fills": filled_info, |
| #2710 | "message": f"Successfully opened SHORT position for {size} {coin}" |
| #2711 | }, indent=2) |
| #2712 | ) |
| #2713 | else: |
| #2714 | return ToolResult(success=False, error=f"Order failed: {result}") |
| #2715 | |
| #2716 | except Exception as e: |
| #2717 | return ToolResult(success=False, error=f"Failed to open short: {str(e)}") |
| #2718 | |
| #2719 | |
| #2720 | class HyperliquidClosePositionTool(Tool): |
| #2721 | """Tool to close a perpetual position on Hyperliquid.""" |
| #2722 | |
| #2723 | @property |
| #2724 | def name(self) -> str: |
| #2725 | return "hyperliquid_close_position" |
| #2726 | |
| #2727 | @property |
| #2728 | def description(self) -> str: |
| #2729 | return """Close an open perpetual position on Hyperliquid DEX. |
| #2730 | Closes entire position or specified size at market price. |
| #2731 | Use this to take profit or cut losses on an existing position.""" |
| #2732 | |
| #2733 | @property |
| #2734 | def parameters(self) -> dict[str, Any]: |
| #2735 | return { |
| #2736 | "type": "object", |
| #2737 | "properties": { |
| #2738 | "coin": { |
| #2739 | "type": "string", |
| #2740 | "description": "Coin to close position for (e.g., 'BTC', 'ETH', 'SOL')", |
| #2741 | }, |
| #2742 | "size": { |
| #2743 | "type": "number", |
| #2744 | "description": "Optional: Size to close. If not specified, closes entire position.", |
| #2745 | }, |
| #2746 | "slippage": { |
| #2747 | "type": "number", |
| #2748 | "description": "Max slippage as decimal (0.05 = 5%). Default: 0.05", |
| #2749 | }, |
| #2750 | }, |
| #2751 | "required": ["coin"], |
| #2752 | } |
| #2753 | |
| #2754 | async def execute(self, coin: str, size: float = None, slippage: float = 0.05) -> ToolResult: |
| #2755 | try: |
| #2756 | hl = get_hyperliquid_client() |
| #2757 | |
| #2758 | result = hl.market_close(coin=coin, size=size, slippage=slippage) |
| #2759 | |
| #2760 | if result.get("status") == "ok": |
| #2761 | statuses = result.get("response", {}).get("data", {}).get("statuses", []) |
| #2762 | |
| #2763 | filled_info = [] |
| #2764 | for status in statuses: |
| #2765 | if "filled" in status: |
| #2766 | filled = status["filled"] |
| #2767 | filled_info.append({ |
| #2768 | "order_id": filled.get("oid"), |
| #2769 | "filled_size": filled.get("totalSz"), |
| #2770 | "avg_price": filled.get("avgPx"), |
| #2771 | }) |
| #2772 | elif "error" in status: |
| #2773 | return ToolResult(success=False, error=f"Order error: {status['error']}") |
| #2774 | |
| #2775 | return ToolResult( |
| #2776 | success=True, |
| #2777 | content=json.dumps({ |
| #2778 | "status": "success", |
| #2779 | "coin": coin, |
| #2780 | "closed_size": size if size else "entire position", |
| #2781 | "fills": filled_info, |
| #2782 | "message": f"Successfully closed {coin} position" |
| #2783 | }, indent=2) |
| #2784 | ) |
| #2785 | else: |
| #2786 | return ToolResult(success=False, error=f"Close failed: {result}") |
| #2787 | |
| #2788 | except Exception as e: |
| #2789 | return ToolResult(success=False, error=f"Failed to close position: {str(e)}") |
| #2790 | |
| #2791 | |
| #2792 | class HyperliquidGetPositionsTool(Tool): |
| #2793 | """Tool to get all open positions on Hyperliquid.""" |
| #2794 | |
| #2795 | @property |
| #2796 | def name(self) -> str: |
| #2797 | return "hyperliquid_get_positions" |
| #2798 | |
| #2799 | @property |
| #2800 | def description(self) -> str: |
| #2801 | return """Get all open perpetual positions on Hyperliquid DEX. |
| #2802 | Shows coin, size (positive=long, negative=short), entry price, unrealized PnL, leverage, and liquidation price.""" |
| #2803 | |
| #2804 | @property |
| #2805 | def parameters(self) -> dict[str, Any]: |
| #2806 | return { |
| #2807 | "type": "object", |
| #2808 | "properties": {}, |
| #2809 | "required": [], |
| #2810 | } |
| #2811 | |
| #2812 | async def execute(self) -> ToolResult: |
| #2813 | try: |
| #2814 | hl = get_hyperliquid_client() |
| #2815 | |
| #2816 | positions = hl.get_positions() |
| #2817 | |
| #2818 | position_list = [ |
| #2819 | { |
| #2820 | "coin": p.coin, |
| #2821 | "side": "LONG" if p.size > 0 else "SHORT", |
| #2822 | "size": abs(p.size), |
| #2823 | "entry_price": p.entry_price, |
| #2824 | "position_value_usd": p.position_value, |
| #2825 | "unrealized_pnl_usd": p.unrealized_pnl, |
| #2826 | "return_on_equity_percent": p.return_on_equity * 100, |
| #2827 | "leverage_type": p.leverage_type, |
| #2828 | "leverage": p.leverage_value, |
| #2829 | "liquidation_price": p.liquidation_price, |
| #2830 | "margin_used_usd": p.margin_used, |
| #2831 | } |
| #2832 | for p in positions |
| #2833 | ] |
| #2834 | |
| #2835 | return ToolResult( |
| #2836 | success=True, |
| #2837 | content=json.dumps({ |
| #2838 | "positions_count": len(position_list), |
| #2839 | "positions": position_list, |
| #2840 | }, indent=2) |
| #2841 | ) |
| #2842 | except Exception as e: |
| #2843 | return ToolResult(success=False, error=f"Failed to get positions: {str(e)}") |
| #2844 | |
| #2845 | |
| #2846 | class HyperliquidSetLeverageTool(Tool): |
| #2847 | """Tool to set leverage for a coin on Hyperliquid.""" |
| #2848 | |
| #2849 | @property |
| #2850 | def name(self) -> str: |
| #2851 | return "hyperliquid_set_leverage" |
| #2852 | |
| #2853 | @property |
| #2854 | def description(self) -> str: |
| #2855 | return """Set the leverage multiplier for a coin on Hyperliquid DEX. |
| #2856 | Higher leverage = higher risk and potential for liquidation. |
| #2857 | Must be set before opening positions. |
| #2858 | Supports cross margin (default) or isolated margin.""" |
| #2859 | |
| #2860 | @property |
| #2861 | def parameters(self) -> dict[str, Any]: |
| #2862 | return { |
| #2863 | "type": "object", |
| #2864 | "properties": { |
| #2865 | "coin": { |
| #2866 | "type": "string", |
| #2867 | "description": "Coin to set leverage for (e.g., 'BTC', 'ETH', 'SOL')", |
| #2868 | }, |
| #2869 | "leverage": { |
| #2870 | "type": "integer", |
| #2871 | "description": "Leverage multiplier (e.g., 5 for 5x leverage)", |
| #2872 | }, |
| #2873 | "is_cross": { |
| #2874 | "type": "boolean", |
| #2875 | "description": "True for cross margin (default), False for isolated margin", |
| #2876 | }, |
| #2877 | }, |
| #2878 | "required": ["coin", "leverage"], |
| #2879 | } |
| #2880 | |
| #2881 | async def execute(self, coin: str, leverage: int, is_cross: bool = True) -> ToolResult: |
| #2882 | try: |
| #2883 | hl = get_hyperliquid_client() |
| #2884 | |
| #2885 | result = hl.update_leverage(coin=coin, leverage=leverage, is_cross=is_cross) |
| #2886 | |
| #2887 | if result.get("status") == "ok": |
| #2888 | return ToolResult( |
| #2889 | success=True, |
| #2890 | content=json.dumps({ |
| #2891 | "status": "success", |
| #2892 | "coin": coin, |
| #2893 | "leverage": leverage, |
| #2894 | "margin_type": "cross" if is_cross else "isolated", |
| #2895 | "message": f"Leverage set to {leverage}x for {coin}" |
| #2896 | }, indent=2) |
| #2897 | ) |
| #2898 | else: |
| #2899 | return ToolResult(success=False, error=f"Failed to set leverage: {result}") |
| #2900 | |
| #2901 | except Exception as e: |
| #2902 | return ToolResult(success=False, error=f"Failed to set leverage: {str(e)}") |
| #2903 | |
| #2904 | |
| #2905 | class HyperliquidPlaceLimitOrderTool(Tool): |
| #2906 | """Tool to place a limit order on Hyperliquid.""" |
| #2907 | |
| #2908 | @property |
| #2909 | def name(self) -> str: |
| #2910 | return "hyperliquid_place_limit_order" |
| #2911 | |
| #2912 | @property |
| #2913 | def description(self) -> str: |
| #2914 | return """Place a limit order on Hyperliquid DEX. |
| #2915 | Order will be placed at the specified price and wait to be filled. |
| #2916 | Use for precise entry/exit points when you don't need immediate execution.""" |
| #2917 | |
| #2918 | @property |
| #2919 | def parameters(self) -> dict[str, Any]: |
| #2920 | return { |
| #2921 | "type": "object", |
| #2922 | "properties": { |
| #2923 | "coin": { |
| #2924 | "type": "string", |
| #2925 | "description": "Coin to trade (e.g., 'BTC', 'ETH', 'SOL')", |
| #2926 | }, |
| #2927 | "is_buy": { |
| #2928 | "type": "boolean", |
| #2929 | "description": "True for buy/long, False for sell/short", |
| #2930 | }, |
| #2931 | "size": { |
| #2932 | "type": "number", |
| #2933 | "description": "Order size in the coin's unit", |
| #2934 | }, |
| #2935 | "price": { |
| #2936 | "type": "number", |
| #2937 | "description": "Limit price for the order", |
| #2938 | }, |
| #2939 | "reduce_only": { |
| #2940 | "type": "boolean", |
| #2941 | "description": "If true, order can only reduce existing position. Default: false", |
| #2942 | }, |
| #2943 | }, |
| #2944 | "required": ["coin", "is_buy", "size", "price"], |
| #2945 | } |
| #2946 | |
| #2947 | async def execute( |
| #2948 | self, |
| #2949 | coin: str, |
| #2950 | is_buy: bool, |
| #2951 | size: float, |
| #2952 | price: float, |
| #2953 | reduce_only: bool = False |
| #2954 | ) -> ToolResult: |
| #2955 | try: |
| #2956 | hl = get_hyperliquid_client() |
| #2957 | |
| #2958 | result = hl.place_order( |
| #2959 | coin=coin, |
| #2960 | is_buy=is_buy, |
| #2961 | size=size, |
| #2962 | limit_price=price, |
| #2963 | order_type="limit", |
| #2964 | reduce_only=reduce_only, |
| #2965 | ) |
| #2966 | |
| #2967 | if result.get("status") == "ok": |
| #2968 | statuses = result.get("response", {}).get("data", {}).get("statuses", []) |
| #2969 | |
| #2970 | order_info = [] |
| #2971 | for status in statuses: |
| #2972 | if "resting" in status: |
| #2973 | order_info.append({ |
| #2974 | "order_id": status["resting"].get("oid"), |
| #2975 | "status": "resting", |
| #2976 | }) |
| #2977 | elif "filled" in status: |
| #2978 | filled = status["filled"] |
| #2979 | order_info.append({ |
| #2980 | "order_id": filled.get("oid"), |
| #2981 | "status": "filled", |
| #2982 | "filled_size": filled.get("totalSz"), |
| #2983 | "avg_price": filled.get("avgPx"), |
| #2984 | }) |
| #2985 | elif "error" in status: |
| #2986 | return ToolResult(success=False, error=f"Order error: {status['error']}") |
| #2987 | |
| #2988 | return ToolResult( |
| #2989 | success=True, |
| #2990 | content=json.dumps({ |
| #2991 | "status": "success", |
| #2992 | "coin": coin, |
| #2993 | "side": "BUY" if is_buy else "SELL", |
| #2994 | "size": size, |
| #2995 | "price": price, |
| #2996 | "reduce_only": reduce_only, |
| #2997 | "orders": order_info, |
| #2998 | "message": f"Limit order placed: {'BUY' if is_buy else 'SELL'} {size} {coin} @ {price}" |
| #2999 | }, indent=2) |
| #3000 | ) |
| #3001 | else: |
| #3002 | return ToolResult(success=False, error=f"Order failed: {result}") |
| #3003 | |
| #3004 | except Exception as e: |
| #3005 | return ToolResult(success=False, error=f"Failed to place order: {str(e)}") |
| #3006 | |
| #3007 | |
| #3008 | class HyperliquidCancelOrderTool(Tool): |
| #3009 | """Tool to cancel an order on Hyperliquid.""" |
| #3010 | |
| #3011 | @property |
| #3012 | def name(self) -> str: |
| #3013 | return "hyperliquid_cancel_order" |
| #3014 | |
| #3015 | @property |
| #3016 | def description(self) -> str: |
| #3017 | return """Cancel an open order on Hyperliquid DEX by order ID. |
| #3018 | Get order IDs from hyperliquid_get_open_orders.""" |
| #3019 | |
| #3020 | @property |
| #3021 | def parameters(self) -> dict[str, Any]: |
| #3022 | return { |
| #3023 | "type": "object", |
| #3024 | "properties": { |
| #3025 | "coin": { |
| #3026 | "type": "string", |
| #3027 | "description": "Coin the order is for (e.g., 'BTC', 'ETH', 'SOL')", |
| #3028 | }, |
| #3029 | "order_id": { |
| #3030 | "type": "integer", |
| #3031 | "description": "Order ID to cancel", |
| #3032 | }, |
| #3033 | }, |
| #3034 | "required": ["coin", "order_id"], |
| #3035 | } |
| #3036 | |
| #3037 | async def execute(self, coin: str, order_id: int) -> ToolResult: |
| #3038 | try: |
| #3039 | hl = get_hyperliquid_client() |
| #3040 | |
| #3041 | result = hl.cancel_order(coin=coin, oid=order_id) |
| #3042 | |
| #3043 | if result.get("status") == "ok": |
| #3044 | return ToolResult( |
| #3045 | success=True, |
| #3046 | content=json.dumps({ |
| #3047 | "status": "success", |
| #3048 | "coin": coin, |
| #3049 | "order_id": order_id, |
| #3050 | "message": f"Order {order_id} cancelled for {coin}" |
| #3051 | }, indent=2) |
| #3052 | ) |
| #3053 | else: |
| #3054 | return ToolResult(success=False, error=f"Cancel failed: {result}") |
| #3055 | |
| #3056 | except Exception as e: |
| #3057 | return ToolResult(success=False, error=f"Failed to cancel order: {str(e)}") |
| #3058 | |
| #3059 | |
| #3060 | class HyperliquidGetOpenOrdersTool(Tool): |
| #3061 | """Tool to get all open orders on Hyperliquid.""" |
| #3062 | |
| #3063 | @property |
| #3064 | def name(self) -> str: |
| #3065 | return "hyperliquid_get_open_orders" |
| #3066 | |
| #3067 | @property |
| #3068 | def description(self) -> str: |
| #3069 | return """Get all open/pending orders on Hyperliquid DEX. |
| #3070 | Shows order ID, coin, side, size, price, and timestamp.""" |
| #3071 | |
| #3072 | @property |
| #3073 | def parameters(self) -> dict[str, Any]: |
| #3074 | return { |
| #3075 | "type": "object", |
| #3076 | "properties": {}, |
| #3077 | "required": [], |
| #3078 | } |
| #3079 | |
| #3080 | async def execute(self) -> ToolResult: |
| #3081 | try: |
| #3082 | hl = get_hyperliquid_client() |
| #3083 | |
| #3084 | orders = hl.get_open_orders() |
| #3085 | |
| #3086 | order_list = [ |
| #3087 | { |
| #3088 | "order_id": o.oid, |
| #3089 | "coin": o.coin, |
| #3090 | "side": "BUY" if o.side == "B" else "SELL", |
| #3091 | "size": o.size, |
| #3092 | "limit_price": o.limit_price, |
| #3093 | "timestamp": o.timestamp, |
| #3094 | } |
| #3095 | for o in orders |
| #3096 | ] |
| #3097 | |
| #3098 | return ToolResult( |
| #3099 | success=True, |
| #3100 | content=json.dumps({ |
| #3101 | "open_orders_count": len(order_list), |
| #3102 | "orders": order_list, |
| #3103 | }, indent=2) |
| #3104 | ) |
| #3105 | except Exception as e: |
| #3106 | return ToolResult(success=False, error=f"Failed to get open orders: {str(e)}") |
| #3107 | |
| #3108 | |
| #3109 | class HyperliquidGetTradeHistoryTool(Tool): |
| #3110 | """Tool to get trade history on Hyperliquid.""" |
| #3111 | |
| #3112 | @property |
| #3113 | def name(self) -> str: |
| #3114 | return "hyperliquid_get_trade_history" |
| #3115 | |
| #3116 | @property |
| #3117 | def description(self) -> str: |
| #3118 | return """Get recent trade history (fills) on Hyperliquid DEX. |
| #3119 | Shows coin, side, size, price, closed PnL, and timestamp.""" |
| #3120 | |
| #3121 | @property |
| #3122 | def parameters(self) -> dict[str, Any]: |
| #3123 | return { |
| #3124 | "type": "object", |
| #3125 | "properties": {}, |
| #3126 | "required": [], |
| #3127 | } |
| #3128 | |
| #3129 | async def execute(self) -> ToolResult: |
| #3130 | try: |
| #3131 | hl = get_hyperliquid_client() |
| #3132 | |
| #3133 | fills = hl.get_user_fills() |
| #3134 | |
| #3135 | trade_list = [ |
| #3136 | { |
| #3137 | "coin": fill.get("coin"), |
| #3138 | "side": fill.get("side"), |
| #3139 | "size": fill.get("sz"), |
| #3140 | "price": fill.get("px"), |
| #3141 | "closed_pnl": fill.get("closedPnl"), |
| #3142 | "direction": fill.get("dir"), |
| #3143 | "timestamp": fill.get("time"), |
| #3144 | "tx_hash": fill.get("hash"), |
| #3145 | } |
| #3146 | for fill in fills[:50] # Limit to 50 most recent |
| #3147 | ] |
| #3148 | |
| #3149 | return ToolResult( |
| #3150 | success=True, |
| #3151 | content=json.dumps({ |
| #3152 | "trades_count": len(trade_list), |
| #3153 | "trades": trade_list, |
| #3154 | }, indent=2) |
| #3155 | ) |
| #3156 | except Exception as e: |
| #3157 | return ToolResult(success=False, error=f"Failed to get trade history: {str(e)}") |
| #3158 | |
| #3159 | |
| #3160 | class HyperliquidGetAvailableCoinsTool(Tool): |
| #3161 | """Tool to get list of available trading coins on Hyperliquid.""" |
| #3162 | |
| #3163 | @property |
| #3164 | def name(self) -> str: |
| #3165 | return "hyperliquid_get_available_coins" |
| #3166 | |
| #3167 | @property |
| #3168 | def description(self) -> str: |
| #3169 | return """Get list of all available coins for perpetual trading on Hyperliquid DEX. |
| #3170 | Useful for discovering what assets can be traded.""" |
| #3171 | |
| #3172 | @property |
| #3173 | def parameters(self) -> dict[str, Any]: |
| #3174 | return { |
| #3175 | "type": "object", |
| #3176 | "properties": {}, |
| #3177 | "required": [], |
| #3178 | } |
| #3179 | |
| #3180 | async def execute(self) -> ToolResult: |
| #3181 | try: |
| #3182 | hl = get_hyperliquid_client() |
| #3183 | |
| #3184 | coins = hl.get_available_coins() |
| #3185 | |
| #3186 | return ToolResult( |
| #3187 | success=True, |
| #3188 | content=json.dumps({ |
| #3189 | "available_coins_count": len(coins), |
| #3190 | "coins": coins, |
| #3191 | }, indent=2) |
| #3192 | ) |
| #3193 | except Exception as e: |
| #3194 | return ToolResult(success=False, error=f"Failed to get available coins: {str(e)}") |
| #3195 | |
| #3196 | |
| #3197 | class HyperliquidTransferTool(Tool): |
| #3198 | """Tool to transfer USDC between perp and spot accounts on Hyperliquid.""" |
| #3199 | |
| #3200 | @property |
| #3201 | def name(self) -> str: |
| #3202 | return "hyperliquid_transfer" |
| #3203 | |
| #3204 | @property |
| #3205 | def description(self) -> str: |
| #3206 | return """Transfer USDC between perpetual and spot accounts on Hyperliquid. |
| #3207 | Use to move funds between your trading accounts.""" |
| #3208 | |
| #3209 | @property |
| #3210 | def parameters(self) -> dict[str, Any]: |
| #3211 | return { |
| #3212 | "type": "object", |
| #3213 | "properties": { |
| #3214 | "amount": { |
| #3215 | "type": "number", |
| #3216 | "description": "Amount of USDC to transfer", |
| #3217 | }, |
| #3218 | "to_perp": { |
| #3219 | "type": "boolean", |
| #3220 | "description": "True to transfer to perp account, False to transfer to spot account", |
| #3221 | }, |
| #3222 | }, |
| #3223 | "required": ["amount", "to_perp"], |
| #3224 | } |
| #3225 | |
| #3226 | async def execute(self, amount: float, to_perp: bool) -> ToolResult: |
| #3227 | try: |
| #3228 | hl = get_hyperliquid_client() |
| #3229 | |
| #3230 | result = hl.usd_class_transfer(amount=amount, to_perp=to_perp) |
| #3231 | |
| #3232 | if result.get("status") == "ok": |
| #3233 | direction = "Spot → Perp" if to_perp else "Perp → Spot" |
| #3234 | return ToolResult( |
| #3235 | success=True, |
| #3236 | content=json.dumps({ |
| #3237 | "status": "success", |
| #3238 | "amount": amount, |
| #3239 | "direction": direction, |
| #3240 | "message": f"Successfully transferred {amount} USDC ({direction})" |
| #3241 | }, indent=2) |
| #3242 | ) |
| #3243 | else: |
| #3244 | return ToolResult(success=False, error=f"Transfer failed: {result}") |
| #3245 | |
| #3246 | except Exception as e: |
| #3247 | return ToolResult(success=False, error=f"Failed to transfer: {str(e)}") |
| #3248 | |
| #3249 | |
| #3250 | class HyperliquidGetAssetIdsTool(Tool): |
| #3251 | """Tool to get asset IDs for Hyperliquid perpetuals and spot markets.""" |
| #3252 | |
| #3253 | @property |
| #3254 | def name(self) -> str: |
| #3255 | return "hyperliquid_get_asset_ids" |
| #3256 | |
| #3257 | @property |
| #3258 | def description(self) -> str: |
| #3259 | return """Get asset IDs for Hyperliquid perpetuals and spot markets. |
| #3260 | |
| #3261 | Asset ID Format: |
| #3262 | - Perpetuals: Integer index from meta (e.g., BTC = 0 on mainnet) |
| #3263 | - Spot: 10000 + spotInfo["index"] (e.g., PURR/USDC = 10000) |
| #3264 | - Builder-deployed perps: 100000 + perp_dex_index * 10000 + index_in_meta |
| #3265 | |
| #3266 | Use this tool to find the correct asset ID when working with Hyperliquid's API. |
| #3267 | Returns both perpetual and spot asset ID mappings.""" |
| #3268 | |
| #3269 | @property |
| #3270 | def parameters(self) -> dict[str, Any]: |
| #3271 | return { |
| #3272 | "type": "object", |
| #3273 | "properties": { |
| #3274 | "coin": { |
| #3275 | "type": "string", |
| #3276 | "description": "Optional: Specific coin to lookup (e.g., 'BTC', 'HYPE', 'PURR/USDC'). If not provided, returns all mappings.", |
| #3277 | }, |
| #3278 | "market_type": { |
| #3279 | "type": "string", |
| #3280 | "enum": ["perp", "spot", "all"], |
| #3281 | "description": "Market type to query: 'perp' for perpetuals, 'spot' for spot, 'all' for both (default: all)", |
| #3282 | }, |
| #3283 | }, |
| #3284 | "required": [], |
| #3285 | } |
| #3286 | |
| #3287 | async def execute(self, coin: str = None, market_type: str = "all") -> ToolResult: |
| #3288 | try: |
| #3289 | hl = get_hyperliquid_client() |
| #3290 | |
| #3291 | result = { |
| #3292 | "asset_id_format": { |
| #3293 | "perpetuals": "index from meta (e.g., BTC = 0)", |
| #3294 | "spot": "10000 + index (e.g., PURR/USDC = 10000)", |
| #3295 | "builder_perps": "100000 + dex_index * 10000 + index" |
| #3296 | } |
| #3297 | } |
| #3298 | |
| #3299 | if coin: |
| #3300 | # Lookup specific coin |
| #3301 | if market_type in ["perp", "all"]: |
| #3302 | perp_id = hl.get_perp_asset_id(coin) |
| #3303 | if perp_id >= 0: |
| #3304 | result["perp_asset_id"] = { |
| #3305 | "coin": coin, |
| #3306 | "asset_id": perp_id |
| #3307 | } |
| #3308 | |
| #3309 | if market_type in ["spot", "all"]: |
| #3310 | spot_id = hl.get_spot_asset_id(coin) |
| #3311 | if spot_id >= 0: |
| #3312 | result["spot_asset_id"] = { |
| #3313 | "coin": coin, |
| #3314 | "asset_id": spot_id, |
| #3315 | "api_format": f"@{spot_id - 10000}" |
| #3316 | } |
| #3317 | else: |
| #3318 | # Return all mappings |
| #3319 | if market_type in ["perp", "all"]: |
| #3320 | perp_ids = hl.get_all_perp_asset_ids() |
| #3321 | result["perpetual_asset_ids"] = { |
| #3322 | "count": len(perp_ids), |
| #3323 | "mappings": dict(list(perp_ids.items())[:50]), # Limit to first 50 |
| #3324 | "note": "Showing first 50. Use coin parameter to lookup specific assets." |
| #3325 | } |
| #3326 | |
| #3327 | if market_type in ["spot", "all"]: |
| #3328 | spot_ids = hl.get_all_spot_asset_ids() |
| #3329 | result["spot_asset_ids"] = { |
| #3330 | "count": len(spot_ids), |
| #3331 | "mappings": {k: {"asset_id": v, "api_format": f"@{v - 10000}"} |
| #3332 | for k, v in list(spot_ids.items())[:30]}, |
| #3333 | "note": "Showing first 30 spot pairs." |
| #3334 | } |
| #3335 | |
| #3336 | return ToolResult( |
| #3337 | success=True, |
| #3338 | content=json.dumps(result, indent=2) |
| #3339 | ) |
| #3340 | except Exception as e: |
| #3341 | return ToolResult(success=False, error=f"Failed to get asset IDs: {str(e)}") |
| #3342 | |
| #3343 | |
| #3344 | class HyperliquidSpotOrderTool(Tool): |
| #3345 | """Tool to place a spot order on Hyperliquid.""" |
| #3346 | |
| #3347 | @property |
| #3348 | def name(self) -> str: |
| #3349 | return "hyperliquid_spot_order" |
| #3350 | |
| #3351 | @property |
| #3352 | def description(self) -> str: |
| #3353 | return """Place a spot order on Hyperliquid DEX. |
| #3354 | |
| #3355 | Spot trading uses different asset IDs than perpetuals: |
| #3356 | - Spot ID = 10000 + spotInfo["index"] |
| #3357 | - Use @{index} format for API calls (e.g., @107 for HYPE) |
| #3358 | |
| #3359 | Common spot pairs: PURR/USDC, HYPE/USDC, etc. |
| #3360 | Use hyperliquid_get_asset_ids to find the correct asset ID.""" |
| #3361 | |
| #3362 | @property |
| #3363 | def parameters(self) -> dict[str, Any]: |
| #3364 | return { |
| #3365 | "type": "object", |
| #3366 | "properties": { |
| #3367 | "coin": { |
| #3368 | "type": "string", |
| #3369 | "description": "Spot pair (e.g., 'HYPE/USDC', 'PURR/USDC') or @index format (e.g., '@107')", |
| #3370 | }, |
| #3371 | "is_buy": { |
| #3372 | "type": "boolean", |
| #3373 | "description": "True to buy, False to sell", |
| #3374 | }, |
| #3375 | "size": { |
| #3376 | "type": "number", |
| #3377 | "description": "Order size in base token", |
| #3378 | }, |
| #3379 | "limit_price": { |
| #3380 | "type": "number", |
| #3381 | "description": "Limit price", |
| #3382 | }, |
| #3383 | "order_type": { |
| #3384 | "type": "string", |
| #3385 | "enum": ["limit", "ioc"], |
| #3386 | "description": "Order type: 'limit' (GTC) or 'ioc' (Immediate or Cancel). Default: limit", |
| #3387 | }, |
| #3388 | }, |
| #3389 | "required": ["coin", "is_buy", "size", "limit_price"], |
| #3390 | } |
| #3391 | |
| #3392 | async def execute( |
| #3393 | self, |
| #3394 | coin: str, |
| #3395 | is_buy: bool, |
| #3396 | size: float, |
| #3397 | limit_price: float, |
| #3398 | order_type: str = "limit" |
| #3399 | ) -> ToolResult: |
| #3400 | try: |
| #3401 | hl = get_hyperliquid_client() |
| #3402 | |
| #3403 | result = hl.spot_order( |
| #3404 | coin=coin, |
| #3405 | is_buy=is_buy, |
| #3406 | size=size, |
| #3407 | limit_price=limit_price, |
| #3408 | order_type=order_type, |
| #3409 | ) |
| #3410 | |
| #3411 | side = "BUY" if is_buy else "SELL" |
| #3412 | |
| #3413 | if result.get("status") == "ok": |
| #3414 | response = result.get("response", {}) |
| #3415 | data = response.get("data", {}) |
| #3416 | statuses = data.get("statuses", []) |
| #3417 | |
| #3418 | if statuses and statuses[0].get("filled"): |
| #3419 | filled = statuses[0]["filled"] |
| #3420 | return ToolResult( |
| #3421 | success=True, |
| #3422 | content=json.dumps({ |
| #3423 | "status": "filled", |
| #3424 | "coin": coin, |
| #3425 | "side": side, |
| #3426 | "size": size, |
| #3427 | "limit_price": limit_price, |
| #3428 | "filled_size": filled.get("totalSz"), |
| #3429 | "avg_price": filled.get("avgPx"), |
| #3430 | "message": f"Spot {side} order filled for {coin}" |
| #3431 | }, indent=2) |
| #3432 | ) |
| #3433 | elif statuses and statuses[0].get("resting"): |
| #3434 | resting = statuses[0]["resting"] |
| #3435 | return ToolResult( |
| #3436 | success=True, |
| #3437 | content=json.dumps({ |
| #3438 | "status": "resting", |
| #3439 | "coin": coin, |
| #3440 | "side": side, |
| #3441 | "size": size, |
| #3442 | "limit_price": limit_price, |
| #3443 | "order_id": resting.get("oid"), |
| #3444 | "message": f"Spot {side} limit order placed for {coin}" |
| #3445 | }, indent=2) |
| #3446 | ) |
| #3447 | |
| #3448 | return ToolResult(success=False, error=f"Spot order failed: {result}") |
| #3449 | |
| #3450 | except Exception as e: |
| #3451 | return ToolResult(success=False, error=f"Failed to place spot order: {str(e)}") |
| #3452 | |
| #3453 | |
| #3454 | class LaunchTokenTool(Tool): |
| #3455 | """Tool to launch a new token on Solana with Bags.fm. Supports uploading local image files.""" |
| #3456 | |
| #3457 | @property |
| #3458 | def name(self) -> str: |
| #3459 | return "launch_token" |
| #3460 | |
| #3461 | @property |
| #3462 | def description(self) -> str: |
| #3463 | return """Launch a new token on Solana with complete metadata and social links. |
| #3464 | Automatically creates token metadata, uploads images, sets up fee sharing, and executes launch transaction. |
| #3465 | Supports uploading local image files (PNG, JPG, JPEG, GIF, WebP up to 15MB) or using existing URLs. |
| #3466 | |
| #3467 | Examples: |
| #3468 | - "Launch a token called MAWD with symbol $MAWD, description 'The MAWDbot token', image from /path/to/logo.png" |
| #3469 | - "Create a new token with name 'Lobster Coin', symbol LOBS, and upload the image at ./lobster.jpg" |
| #3470 | - "Launch token named SolBot using image URL https://example.com/logo.png" |
| #3471 | """ |
| #3472 | |
| #3473 | @property |
| #3474 | def parameters(self) -> dict[str, Any]: |
| #3475 | return { |
| #3476 | "type": "object", |
| #3477 | "properties": { |
| #3478 | "name": { |
| #3479 | "type": "string", |
| #3480 | "description": "Token name", |
| #3481 | }, |
| #3482 | "symbol": { |
| #3483 | "type": "string", |
| #3484 | "description": "Token symbol (e.g., 'MAWD', '$MAWD')", |
| #3485 | }, |
| #3486 | "description": { |
| #3487 | "type": "string", |
| #3488 | "description": "Token description", |
| #3489 | }, |
| #3490 | "image_url": { |
| #3491 | "type": "string", |
| #3492 | "description": "URL to existing token image (mutually exclusive with image_file_path)", |
| #3493 | }, |
| #3494 | "image_file_path": { |
| #3495 | "type": "string", |
| #3496 | "description": "Path to local image file to upload (PNG, JPG, JPEG, GIF, WebP, max 15MB). Mutually exclusive with image_url.", |
| #3497 | }, |
| #3498 | "initial_buy_sol": { |
| #3499 | "type": "number", |
| #3500 | "description": "Initial buy amount in SOL (default: 0.01)", |
| #3501 | }, |
| #3502 | "twitter": { |
| #3503 | "type": "string", |
| #3504 | "description": "Twitter/X URL (optional)", |
| #3505 | }, |
| #3506 | "website": { |
| #3507 | "type": "string", |
| #3508 | "description": "Website URL (optional)", |
| #3509 | }, |
| #3510 | "telegram": { |
| #3511 | "type": "string", |
| #3512 | "description": "Telegram URL (optional)", |
| #3513 | }, |
| #3514 | "fee_share_bps": { |
| #3515 | "type": "integer", |
| #3516 | "description": "Creator fee share in basis points (default: 10000 = 100%)", |
| #3517 | }, |
| #3518 | }, |
| #3519 | "required": ["name", "symbol", "description"], |
| #3520 | } |
| #3521 | |
| #3522 | async def execute( |
| #3523 | self, |
| #3524 | name: str, |
| #3525 | symbol: str, |
| #3526 | description: str, |
| #3527 | image_url: Optional[str] = None, |
| #3528 | image_file_path: Optional[str] = None, |
| #3529 | initial_buy_sol: float = 0.01, |
| #3530 | twitter: Optional[str] = None, |
| #3531 | website: Optional[str] = None, |
| #3532 | telegram: Optional[str] = None, |
| #3533 | fee_share_bps: int = 10000, |
| #3534 | ) -> ToolResult: |
| #3535 | try: |
| #3536 | bags = get_bags_client() |
| #3537 | |
| #3538 | # Ensure at least one image source is provided |
| #3539 | if not image_url and not image_file_path: |
| #3540 | return ToolResult( |
| #3541 | success=False, |
| #3542 | error="Either image_url or image_file_path must be provided" |
| #3543 | ) |
| #3544 | |
| #3545 | # Launch token with Bags (supports both URL and file upload) |
| #3546 | result = await bags.launch_token( |
| #3547 | name=name, |
| #3548 | symbol=symbol, |
| #3549 | description=description, |
| #3550 | image_url=image_url, |
| #3551 | image_file_path=image_file_path, |
| #3552 | initial_buy_sol=initial_buy_sol, |
| #3553 | twitter=twitter, |
| #3554 | website=website, |
| #3555 | telegram=telegram, |
| #3556 | fee_share_bps=fee_share_bps, |
| #3557 | ) |
| #3558 | |
| #3559 | return ToolResult( |
| #3560 | success=True, |
| #3561 | content=json.dumps({ |
| #3562 | "status": "success", |
| #3563 | "token_mint": result["token_mint"], |
| #3564 | "metadata_uri": result["metadata_uri"], |
| #3565 | "config_key": result["config_key"], |
| #3566 | "signature": result["signature"], |
| #3567 | "token_url": result["token_url"], |
| #3568 | "initial_buy_sol": initial_buy_sol, |
| #3569 | "image_source": "file_upload" if image_file_path else "url", |
| #3570 | "message": f"Token {name} ({symbol}) launched successfully! View at {result['token_url']}" |
| #3571 | }, indent=2) |
| #3572 | ) |
| #3573 | |
| #3574 | except FileNotFoundError as e: |
| #3575 | return ToolResult(success=False, error=f"Image file not found: {str(e)}") |
| #3576 | except ValueError as e: |
| #3577 | return ToolResult(success=False, error=f"Invalid input: {str(e)}") |
| #3578 | except Exception as e: |
| #3579 | return ToolResult(success=False, error=f"Failed to launch token: {str(e)}") |
| #3580 | |
| #3581 | |
| #3582 | # ============================================== |
| #3583 | # CDP (Coinbase Developer Platform) Tools |
| #3584 | # ============================================== |
| #3585 | |
| #3586 | class CDPCreateAccountTool(Tool): |
| #3587 | """Tool to create a new Solana account via CDP.""" |
| #3588 | |
| #3589 | @property |
| #3590 | def name(self) -> str: |
| #3591 | return "cdp_create_account" |
| #3592 | |
| #3593 | @property |
| #3594 | def description(self) -> str: |
| #3595 | return """Create a new Solana account managed by Coinbase Developer Platform (CDP). |
| #3596 | |
| #3597 | This creates a secure, custodial Solana account on devnet that can be used for: |
| #3598 | - Testing and development |
| #3599 | - Receiving devnet SOL from faucets |
| #3600 | - Signing transactions with CDP-managed keys |
| #3601 | |
| #3602 | The account is managed by CDP, so private keys never need to be exposed. |
| #3603 | Returns the new account address. |
| #3604 | |
| #3605 | Note: This creates accounts on Solana devnet by default.""" |
| #3606 | |
| #3607 | @property |
| #3608 | def parameters(self) -> dict[str, Any]: |
| #3609 | return { |
| #3610 | "type": "object", |
| #3611 | "properties": { |
| #3612 | "name": { |
| #3613 | "type": "string", |
| #3614 | "description": "Optional unique name for the account (2-36 chars, alphanumeric + hyphens)", |
| #3615 | }, |
| #3616 | }, |
| #3617 | "required": [], |
| #3618 | } |
| #3619 | |
| #3620 | async def execute(self, name: Optional[str] = None) -> ToolResult: |
| #3621 | cdp = get_cdp_client() |
| #3622 | if cdp is None: |
| #3623 | return ToolResult( |
| #3624 | success=False, |
| #3625 | error="CDP client not configured. Set CDP_API_KEY_ID and CDP_API_KEY_SECRET environment variables." |
| #3626 | ) |
| #3627 | |
| #3628 | try: |
| #3629 | result = await cdp.create_account(name=name) |
| #3630 | content = "✓ Created new CDP Solana account:\n" |
| #3631 | content += f" Address: {result['address']}\n" |
| #3632 | if result.get('name'): |
| #3633 | content += f" Name: {result['name']}\n" |
| #3634 | content += "\nThis account is managed by CDP and can be funded via the devnet faucet." |
| #3635 | |
| #3636 | return ToolResult(success=True, content=content) |
| #3637 | except Exception as e: |
| #3638 | return ToolResult(success=False, error=f"Failed to create CDP account: {str(e)}") |
| #3639 | |
| #3640 | |
| #3641 | class CDPRequestFaucetTool(Tool): |
| #3642 | """Tool to request devnet SOL from the faucet for a CDP account.""" |
| #3643 | |
| #3644 | @property |
| #3645 | def name(self) -> str: |
| #3646 | return "cdp_request_faucet" |
| #3647 | |
| #3648 | @property |
| #3649 | def description(self) -> str: |
| #3650 | return """Request devnet SOL from the Solana faucet for a CDP-managed account. |
| #3651 | |
| #3652 | This funds a Solana devnet account with test SOL (~0.001-0.01 SOL). |
| #3653 | Use this after creating a CDP account to give it an initial balance for testing. |
| #3654 | |
| #3655 | Returns the transaction signature and explorer link.""" |
| #3656 | |
| #3657 | @property |
| #3658 | def parameters(self) -> dict[str, Any]: |
| #3659 | return { |
| #3660 | "type": "object", |
| #3661 | "properties": { |
| #3662 | "address": { |
| #3663 | "type": "string", |
| #3664 | "description": "Solana address to fund (must be a valid base58 address)", |
| #3665 | }, |
| #3666 | }, |
| #3667 | "required": ["address"], |
| #3668 | } |
| #3669 | |
| #3670 | async def execute(self, address: str) -> ToolResult: |
| #3671 | cdp = get_cdp_client() |
| #3672 | if cdp is None: |
| #3673 | return ToolResult( |
| #3674 | success=False, |
| #3675 | error="CDP client not configured." |
| #3676 | ) |
| #3677 | |
| #3678 | try: |
| #3679 | result = await cdp.request_faucet(address, token="sol") |
| #3680 | content = "✓ Faucet request successful!\n" |
| #3681 | content += f" Transaction: {result['transaction_signature']}\n" |
| #3682 | content += f" Explorer: {result['explorer_url']}\n" |
| #3683 | content += "\nWait 10-30 seconds for the funds to arrive." |
| #3684 | |
| #3685 | return ToolResult(success=True, content=content) |
| #3686 | except Exception as e: |
| #3687 | return ToolResult(success=False, error=f"Faucet request failed: {str(e)}") |
| #3688 | |
| #3689 | |
| #3690 | class CDPGetBalanceTool(Tool): |
| #3691 | """Tool to get SOL balance for a CDP account.""" |
| #3692 | |
| #3693 | @property |
| #3694 | def name(self) -> str: |
| #3695 | return "cdp_get_balance" |
| #3696 | |
| #3697 | @property |
| #3698 | def description(self) -> str: |
| #3699 | return """Get the SOL balance for any Solana address. |
| #3700 | |
| #3701 | Returns the balance in both SOL and lamports.""" |
| #3702 | |
| #3703 | @property |
| #3704 | def parameters(self) -> dict[str, Any]: |
| #3705 | return { |
| #3706 | "type": "object", |
| #3707 | "properties": { |
| #3708 | "address": { |
| #3709 | "type": "string", |
| #3710 | "description": "Solana address to check balance for", |
| #3711 | }, |
| #3712 | }, |
| #3713 | "required": ["address"], |
| #3714 | } |
| #3715 | |
| #3716 | async def execute(self, address: str) -> ToolResult: |
| #3717 | cdp = get_cdp_client() |
| #3718 | if cdp is None: |
| #3719 | return ToolResult( |
| #3720 | success=False, |
| #3721 | error="CDP client not configured." |
| #3722 | ) |
| #3723 | |
| #3724 | try: |
| #3725 | result = await cdp.get_balance(address) |
| #3726 | content = f"Balance for {result['address']}:\n" |
| #3727 | content += f" {result['balance_sol']:.6f} SOL\n" |
| #3728 | content += f" ({result['balance_lamports']:,} lamports)" |
| #3729 | |
| #3730 | return ToolResult(success=True, content=content) |
| #3731 | except Exception as e: |
| #3732 | return ToolResult(success=False, error=f"Failed to get balance: {str(e)}") |
| #3733 | |
| #3734 | |
| #3735 | class CDPSendSOLTool(Tool): |
| #3736 | """Tool to send SOL from a CDP-managed account.""" |
| #3737 | |
| #3738 | @property |
| #3739 | def name(self) -> str: |
| #3740 | return "cdp_send_sol" |
| #3741 | |
| #3742 | @property |
| #3743 | def description(self) -> str: |
| #3744 | return """Send SOL from a CDP-managed account to another address. |
| #3745 | |
| #3746 | This uses CDP to securely sign and send a transaction without exposing private keys. |
| #3747 | Works with CDP-managed accounts on devnet. |
| #3748 | |
| #3749 | Returns transaction signature and explorer link.""" |
| #3750 | |
| #3751 | @property |
| #3752 | def parameters(self) -> dict[str, Any]: |
| #3753 | return { |
| #3754 | "type": "object", |
| #3755 | "properties": { |
| #3756 | "from_address": { |
| #3757 | "type": "string", |
| #3758 | "description": "Sender address (must be a CDP-managed account)", |
| #3759 | }, |
| #3760 | "to_address": { |
| #3761 | "type": "string", |
| #3762 | "description": "Recipient address (any valid Solana address)", |
| #3763 | }, |
| #3764 | "sol_amount": { |
| #3765 | "type": "number", |
| #3766 | "description": "Amount of SOL to send (e.g., 0.001 for 0.001 SOL)", |
| #3767 | }, |
| #3768 | }, |
| #3769 | "required": ["from_address", "to_address", "sol_amount"], |
| #3770 | } |
| #3771 | |
| #3772 | async def execute(self, from_address: str, to_address: str, sol_amount: float) -> ToolResult: |
| #3773 | cdp = get_cdp_client() |
| #3774 | if cdp is None: |
| #3775 | return ToolResult( |
| #3776 | success=False, |
| #3777 | error="CDP client not configured." |
| #3778 | ) |
| #3779 | |
| #3780 | try: |
| #3781 | # Convert SOL to lamports |
| #3782 | lamports = int(sol_amount * 1e9) |
| #3783 | |
| #3784 | result = await cdp.send_sol( |
| #3785 | from_address=from_address, |
| #3786 | to_address=to_address, |
| #3787 | lamports=lamports, |
| #3788 | ) |
| #3789 | |
| #3790 | content = "✓ Transaction successful!\n" |
| #3791 | content += f" From: {result['from_address']}\n" |
| #3792 | content += f" To: {result['to_address']}\n" |
| #3793 | content += f" Amount: {result['sol_amount']:.6f} SOL\n" |
| #3794 | content += f" Signature: {result['signature']}\n" |
| #3795 | content += f" Explorer: {result['explorer_url']}" |
| #3796 | |
| #3797 | return ToolResult(success=True, content=content) |
| #3798 | except Exception as e: |
| #3799 | return ToolResult(success=False, error=f"Failed to send SOL: {str(e)}") |
| #3800 | |
| #3801 | |
| #3802 | class CDPListAccountsTool(Tool): |
| #3803 | """Tool to list all CDP-managed Solana accounts.""" |
| #3804 | |
| #3805 | @property |
| #3806 | def name(self) -> str: |
| #3807 | return "cdp_list_accounts" |
| #3808 | |
| #3809 | @property |
| #3810 | def description(self) -> str: |
| #3811 | return """List all Solana accounts managed by CDP in this project. |
| #3812 | |
| #3813 | Returns a list of account addresses and their names (if set).""" |
| #3814 | |
| #3815 | @property |
| #3816 | def parameters(self) -> dict[str, Any]: |
| #3817 | return { |
| #3818 | "type": "object", |
| #3819 | "properties": {}, |
| #3820 | "required": [], |
| #3821 | } |
| #3822 | |
| #3823 | async def execute(self) -> ToolResult: |
| #3824 | cdp = get_cdp_client() |
| #3825 | if cdp is None: |
| #3826 | return ToolResult( |
| #3827 | success=False, |
| #3828 | error="CDP client not configured." |
| #3829 | ) |
| #3830 | |
| #3831 | try: |
| #3832 | accounts = await cdp.list_accounts() |
| #3833 | |
| #3834 | if not accounts: |
| #3835 | return ToolResult(success=True, content="No CDP accounts found.") |
| #3836 | |
| #3837 | content = f"CDP-Managed Solana Accounts ({len(accounts)}):\n\n" |
| #3838 | for i, acc in enumerate(accounts, 1): |
| #3839 | content += f"{i}. {acc['address']}" |
| #3840 | if acc.get('name'): |
| #3841 | content += f" (Name: {acc['name']})" |
| #3842 | content += "\n" |
| #3843 | |
| #3844 | return ToolResult(success=True, content=content) |
| #3845 | except Exception as e: |
| #3846 | return ToolResult(success=False, error=f"Failed to list accounts: {str(e)}") |
| #3847 | |
| #3848 | |
| #3849 | # ============================================================ |
| #3850 | # COINGECKO TOOLS - Real-time Cryptocurrency Market Data |
| #3851 | # ============================================================ |
| #3852 | |
| #3853 | class GetCryptoPriceTool(Tool): |
| #3854 | """Get current prices for cryptocurrencies.""" |
| #3855 | |
| #3856 | @property |
| #3857 | def name(self) -> str: |
| #3858 | return "get_crypto_price" |
| #3859 | |
| #3860 | @property |
| #3861 | def description(self) -> str: |
| #3862 | return "Get current prices, market cap, volume, and 24hr change for cryptocurrencies by their IDs (e.g., 'bitcoin', 'ethereum', 'solana')." |
| #3863 | |
| #3864 | @property |
| #3865 | def parameters(self) -> dict[str, Any]: |
| #3866 | return { |
| #3867 | "type": "object", |
| #3868 | "properties": { |
| #3869 | "coin_ids": { |
| #3870 | "type": "array", |
| #3871 | "items": {"type": "string"}, |
| #3872 | "description": "Coin IDs (e.g., ['bitcoin', 'ethereum', 'solana'])" |
| #3873 | }, |
| #3874 | "vs_currency": { |
| #3875 | "type": "string", |
| #3876 | "description": "Target currency (default: usd)", |
| #3877 | "default": "usd" |
| #3878 | } |
| #3879 | }, |
| #3880 | "required": ["coin_ids"] |
| #3881 | } |
| #3882 | |
| #3883 | async def execute(self, coin_ids: list[str], vs_currency: str = "usd") -> ToolResult: |
| #3884 | cg = get_coingecko_client() |
| #3885 | if cg is None: |
| #3886 | return ToolResult(success=False, error="CoinGecko client not configured. Set COINGECKO_API_KEY in .env") |
| #3887 | |
| #3888 | try: |
| #3889 | data = await cg.get_price( |
| #3890 | coin_ids=coin_ids, |
| #3891 | vs_currencies=[vs_currency], |
| #3892 | include_market_cap=True, |
| #3893 | include_24hr_vol=True, |
| #3894 | include_24hr_change=True |
| #3895 | ) |
| #3896 | |
| #3897 | content = f"💰 Cryptocurrency Prices ({vs_currency.upper()}):\n\n" |
| #3898 | for coin_id in coin_ids: |
| #3899 | if coin_id in data: |
| #3900 | coin_data = data[coin_id] |
| #3901 | price = coin_data.get(vs_currency, 0) |
| #3902 | mcap = coin_data.get(f"{vs_currency}_market_cap", 0) |
| #3903 | vol = coin_data.get(f"{vs_currency}_24h_vol", 0) |
| #3904 | change = coin_data.get(f"{vs_currency}_24h_change", 0) |
| #3905 | |
| #3906 | content += f"🪙 {coin_id.upper()}\n" |
| #3907 | content += f" Price: ${price:,.2f}\n" |
| #3908 | if mcap: |
| #3909 | content += f" Market Cap: ${mcap:,.0f}\n" |
| #3910 | if vol: |
| #3911 | content += f" 24h Volume: ${vol:,.0f}\n" |
| #3912 | if change is not None: |
| #3913 | emoji = "📈" if change > 0 else "📉" |
| #3914 | content += f" 24h Change: {emoji} {change:.2f}%\n" |
| #3915 | content += "\n" |
| #3916 | else: |
| #3917 | content += f"❌ {coin_id}: Not found\n\n" |
| #3918 | |
| #3919 | return ToolResult(success=True, content=content) |
| #3920 | except Exception as e: |
| #3921 | return ToolResult(success=False, error=f"Failed to get prices: {str(e)}") |
| #3922 | |
| #3923 | |
| #3924 | class GetCoinMarketDataTool(Tool): |
| #3925 | """Get detailed market data for cryptocurrencies.""" |
| #3926 | |
| #3927 | @property |
| #3928 | def name(self) -> str: |
| #3929 | return "get_coin_market_data" |
| #3930 | |
| #3931 | @property |
| #3932 | def description(self) -> str: |
| #3933 | return "Get comprehensive market data for cryptocurrencies including price, market cap, volume, supply, ATH/ATL, and more." |
| #3934 | |
| #3935 | @property |
| #3936 | def parameters(self) -> dict[str, Any]: |
| #3937 | return { |
| #3938 | "type": "object", |
| #3939 | "properties": { |
| #3940 | "vs_currency": { |
| #3941 | "type": "string", |
| #3942 | "description": "Target currency (default: usd)", |
| #3943 | "default": "usd" |
| #3944 | }, |
| #3945 | "coin_ids": { |
| #3946 | "type": "array", |
| #3947 | "items": {"type": "string"}, |
| #3948 | "description": "Filter by specific coin IDs (optional)", |
| #3949 | "default": None |
| #3950 | }, |
| #3951 | "category": { |
| #3952 | "type": "string", |
| #3953 | "description": "Filter by category (e.g., 'layer-1', 'meme-token', 'defi')", |
| #3954 | "default": None |
| #3955 | }, |
| #3956 | "order": { |
| #3957 | "type": "string", |
| #3958 | "description": "Sort order: market_cap_desc, volume_desc, etc.", |
| #3959 | "default": "market_cap_desc" |
| #3960 | }, |
| #3961 | "per_page": { |
| #3962 | "type": "integer", |
| #3963 | "description": "Results per page (max 250)", |
| #3964 | "default": 10 |
| #3965 | } |
| #3966 | }, |
| #3967 | "required": [] |
| #3968 | } |
| #3969 | |
| #3970 | async def execute( |
| #3971 | self, |
| #3972 | vs_currency: str = "usd", |
| #3973 | coin_ids: Optional[list[str]] = None, |
| #3974 | category: Optional[str] = None, |
| #3975 | order: str = "market_cap_desc", |
| #3976 | per_page: int = 10 |
| #3977 | ) -> ToolResult: |
| #3978 | cg = get_coingecko_client() |
| #3979 | if cg is None: |
| #3980 | return ToolResult(success=False, error="CoinGecko client not configured. Set COINGECKO_API_KEY in .env") |
| #3981 | |
| #3982 | try: |
| #3983 | data = await cg.get_coin_markets( |
| #3984 | vs_currency=vs_currency, |
| #3985 | ids=coin_ids, |
| #3986 | category=category, |
| #3987 | order=order, |
| #3988 | per_page=min(per_page, 250), |
| #3989 | price_change_percentage="1h,24h,7d" |
| #3990 | ) |
| #3991 | |
| #3992 | if not data: |
| #3993 | return ToolResult(success=True, content="No market data found.") |
| #3994 | |
| #3995 | content = f"📊 Cryptocurrency Market Data ({vs_currency.upper()}):\n\n" |
| #3996 | for i, coin in enumerate(data, 1): |
| #3997 | content += f"{i}. {coin['name']} ({coin['symbol'].upper()})\n" |
| #3998 | content += f" Price: ${coin['current_price']:,.2f}\n" |
| #3999 | content += f" Market Cap: ${coin['market_cap']:,.0f} (Rank #{coin['market_cap_rank']})\n" |
| #4000 | content += f" 24h Volume: ${coin['total_volume']:,.0f}\n" |
| #4001 | |
| #4002 | if coin.get('price_change_percentage_1h_in_currency') is not None: |
| #4003 | change_1h = coin['price_change_percentage_1h_in_currency'] |
| #4004 | emoji = "📈" if change_1h > 0 else "📉" |
| #4005 | content += f" 1h Change: {emoji} {change_1h:.2f}%\n" |
| #4006 | |
| #4007 | if coin.get('price_change_percentage_24h') is not None: |
| #4008 | change_24h = coin['price_change_percentage_24h'] |
| #4009 | emoji = "📈" if change_24h > 0 else "📉" |
| #4010 | content += f" 24h Change: {emoji} {change_24h:.2f}%\n" |
| #4011 | |
| #4012 | if coin.get('price_change_percentage_7d_in_currency') is not None: |
| #4013 | change_7d = coin['price_change_percentage_7d_in_currency'] |
| #4014 | emoji = "📈" if change_7d > 0 else "📉" |
| #4015 | content += f" 7d Change: {emoji} {change_7d:.2f}%\n" |
| #4016 | |
| #4017 | content += "\n" |
| #4018 | |
| #4019 | return ToolResult(success=True, content=content) |
| #4020 | except Exception as e: |
| #4021 | return ToolResult(success=False, error=f"Failed to get market data: {str(e)}") |
| #4022 | |
| #4023 | |
| #4024 | class GetTrendingCryptosTool(Tool): |
| #4025 | """Get trending cryptocurrencies in the last 24 hours.""" |
| #4026 | |
| #4027 | @property |
| #4028 | def name(self) -> str: |
| #4029 | return "get_trending_cryptos" |
| #4030 | |
| #4031 | @property |
| #4032 | def description(self) -> str: |
| #4033 | return "Get the top trending cryptocurrencies based on search activity in the last 24 hours." |
| #4034 | |
| #4035 | @property |
| #4036 | def parameters(self) -> dict[str, Any]: |
| #4037 | return { |
| #4038 | "type": "object", |
| #4039 | "properties": {}, |
| #4040 | "required": [] |
| #4041 | } |
| #4042 | |
| #4043 | async def execute(self) -> ToolResult: |
| #4044 | cg = get_coingecko_client() |
| #4045 | if cg is None: |
| #4046 | return ToolResult(success=False, error="CoinGecko client not configured. Set COINGECKO_API_KEY in .env") |
| #4047 | |
| #4048 | try: |
| #4049 | data = await cg.get_trending() |
| #4050 | coins = data.get('coins', []) |
| #4051 | |
| #4052 | if not coins: |
| #4053 | return ToolResult(success=True, content="No trending coins found.") |
| #4054 | |
| #4055 | content = "🔥 Trending Cryptocurrencies (Last 24h):\n\n" |
| #4056 | for i, item in enumerate(coins, 1): |
| #4057 | coin = item.get('item', {}) |
| #4058 | content += f"{i}. {coin.get('name')} ({coin.get('symbol', '').upper()})\n" |
| #4059 | content += f" Rank: #{coin.get('market_cap_rank', 'N/A')}\n" |
| #4060 | |
| #4061 | if coin.get('price_btc'): |
| #4062 | content += f" Price (BTC): {coin['price_btc']:.10f}\n" |
| #4063 | |
| #4064 | if coin.get('data', {}).get('price'): |
| #4065 | price_usd = coin['data']['price'] |
| #4066 | content += f" Price (USD): ${price_usd:,.8f}\n" |
| #4067 | |
| #4068 | if coin.get('data', {}).get('price_change_percentage_24h'): |
| #4069 | change = coin['data']['price_change_percentage_24h'].get('usd', 0) |
| #4070 | emoji = "📈" if change > 0 else "📉" |
| #4071 | content += f" 24h Change: {emoji} {change:.2f}%\n" |
| #4072 | |
| #4073 | content += "\n" |
| #4074 | |
| #4075 | return ToolResult(success=True, content=content) |
| #4076 | except Exception as e: |
| #4077 | return ToolResult(success=False, error=f"Failed to get trending cryptos: {str(e)}") |
| #4078 | |
| #4079 | |
| #4080 | class SearchCryptoTool(Tool): |
| #4081 | """Search for cryptocurrencies by name or symbol.""" |
| #4082 | |
| #4083 | @property |
| #4084 | def name(self) -> str: |
| #4085 | return "search_crypto" |
| #4086 | |
| #4087 | @property |
| #4088 | def description(self) -> str: |
| #4089 | return "Search for cryptocurrencies, categories, and markets by name or symbol. Returns coin IDs needed for other CoinGecko tools." |
| #4090 | |
| #4091 | @property |
| #4092 | def parameters(self) -> dict[str, Any]: |
| #4093 | return { |
| #4094 | "type": "object", |
| #4095 | "properties": { |
| #4096 | "query": { |
| #4097 | "type": "string", |
| #4098 | "description": "Search query (coin name or symbol)" |
| #4099 | } |
| #4100 | }, |
| #4101 | "required": ["query"] |
| #4102 | } |
| #4103 | |
| #4104 | async def execute(self, query: str) -> ToolResult: |
| #4105 | cg = get_coingecko_client() |
| #4106 | if cg is None: |
| #4107 | return ToolResult(success=False, error="CoinGecko client not configured. Set COINGECKO_API_KEY in .env") |
| #4108 | |
| #4109 | try: |
| #4110 | data = await cg.search_coins(query) |
| #4111 | coins = data.get('coins', []) |
| #4112 | |
| #4113 | if not coins: |
| #4114 | return ToolResult(success=True, content=f"No results found for '{query}'.") |
| #4115 | |
| #4116 | content = f"🔍 Search Results for '{query}':\n\n" |
| #4117 | for i, coin in enumerate(coins[:10], 1): # Limit to top 10 results |
| #4118 | content += f"{i}. {coin['name']} ({coin['symbol'].upper()})\n" |
| #4119 | content += f" ID: {coin['id']}\n" |
| #4120 | content += f" Rank: #{coin.get('market_cap_rank', 'N/A')}\n" |
| #4121 | content += "\n" |
| #4122 | |
| #4123 | if len(coins) > 10: |
| #4124 | content += f"... and {len(coins) - 10} more results\n" |
| #4125 | |
| #4126 | return ToolResult(success=True, content=content) |
| #4127 | except Exception as e: |
| #4128 | return ToolResult(success=False, error=f"Failed to search: {str(e)}") |
| #4129 | |
| #4130 | |
| #4131 | class GetGlobalCryptoStatsTool(Tool): |
| #4132 | """Get global cryptocurrency market statistics.""" |
| #4133 | |
| #4134 | @property |
| #4135 | def name(self) -> str: |
| #4136 | return "get_global_crypto_stats" |
| #4137 | |
| #4138 | @property |
| #4139 | def description(self) -> str: |
| #4140 | return "Get global cryptocurrency market data including total market cap, volume, BTC dominance, and market trends." |
| #4141 | |
| #4142 | @property |
| #4143 | def parameters(self) -> dict[str, Any]: |
| #4144 | return { |
| #4145 | "type": "object", |
| #4146 | "properties": {}, |
| #4147 | "required": [] |
| #4148 | } |
| #4149 | |
| #4150 | async def execute(self) -> ToolResult: |
| #4151 | cg = get_coingecko_client() |
| #4152 | if cg is None: |
| #4153 | return ToolResult(success=False, error="CoinGecko client not configured. Set COINGECKO_API_KEY in .env") |
| #4154 | |
| #4155 | try: |
| #4156 | data = await cg.get_global_data() |
| #4157 | global_data = data.get('data', {}) |
| #4158 | |
| #4159 | content = "🌍 Global Cryptocurrency Market Statistics:\n\n" |
| #4160 | |
| #4161 | total_mcap = global_data.get('total_market_cap', {}).get('usd', 0) |
| #4162 | total_vol = global_data.get('total_volume', {}).get('usd', 0) |
| #4163 | btc_dominance = global_data.get('market_cap_percentage', {}).get('btc', 0) |
| #4164 | eth_dominance = global_data.get('market_cap_percentage', {}).get('eth', 0) |
| #4165 | active_cryptos = global_data.get('active_cryptocurrencies', 0) |
| #4166 | markets = global_data.get('markets', 0) |
| #4167 | |
| #4168 | content += f"💰 Total Market Cap: ${total_mcap:,.0f}\n" |
| #4169 | content += f"📊 Total 24h Volume: ${total_vol:,.0f}\n" |
| #4170 | content += f"₿ BTC Dominance: {btc_dominance:.2f}%\n" |
| #4171 | content += f"Ξ ETH Dominance: {eth_dominance:.2f}%\n" |
| #4172 | content += f"🪙 Active Cryptocurrencies: {active_cryptos:,}\n" |
| #4173 | content += f"🏪 Markets: {markets:,}\n" |
| #4174 | |
| #4175 | mcap_change = global_data.get('market_cap_change_percentage_24h_usd', 0) |
| #4176 | if mcap_change: |
| #4177 | emoji = "📈" if mcap_change > 0 else "📉" |
| #4178 | content += f"\n24h Market Cap Change: {emoji} {mcap_change:.2f}%\n" |
| #4179 | |
| #4180 | return ToolResult(success=True, content=content) |
| #4181 | except Exception as e: |
| #4182 | return ToolResult(success=False, error=f"Failed to get global stats: {str(e)}") |
| #4183 | |
| #4184 | |
| #4185 | # ============================================== |
| #4186 | # PumpFun Tools |
| #4187 | # ============================================== |
| #4188 | |
| #4189 | class PumpFunLaunchTokenTool(Tool): |
| #4190 | """Tool to launch a new token on pump.fun bonding curve.""" |
| #4191 | |
| #4192 | @property |
| #4193 | def name(self) -> str: |
| #4194 | return "pumpfun_launch_token" |
| #4195 | |
| #4196 | @property |
| #4197 | def description(self) -> str: |
| #4198 | return """Launch a new token on pump.fun with a bonding curve. |
| #4199 | |
| #4200 | Creates a new SPL token that is instantly tradeable on pump.fun's bonding curve. |
| #4201 | When the token hits ~$69k market cap, liquidity automatically migrates to Raydium. |
| #4202 | |
| #4203 | Examples: |
| #4204 | - "Launch a memecoin called PEPE2 on pump.fun" |
| #4205 | - "Create a pump.fun token with name CatCoin, symbol CAT" |
| #4206 | """ |
| #4207 | |
| #4208 | @property |
| #4209 | def parameters(self) -> dict[str, Any]: |
| #4210 | return { |
| #4211 | "type": "object", |
| #4212 | "properties": { |
| #4213 | "name": { |
| #4214 | "type": "string", |
| #4215 | "description": "Token name (e.g., 'Doge Coin')", |
| #4216 | }, |
| #4217 | "symbol": { |
| #4218 | "type": "string", |
| #4219 | "description": "Token symbol/ticker (e.g., 'DOGE')", |
| #4220 | }, |
| #4221 | "description": { |
| #4222 | "type": "string", |
| #4223 | "description": "Token description", |
| #4224 | }, |
| #4225 | "image_url": { |
| #4226 | "type": "string", |
| #4227 | "description": "URL to token image (optional if image_path provided)", |
| #4228 | }, |
| #4229 | "image_path": { |
| #4230 | "type": "string", |
| #4231 | "description": "Local path to token image file (optional if image_url provided)", |
| #4232 | }, |
| #4233 | "twitter": { |
| #4234 | "type": "string", |
| #4235 | "description": "Twitter/X URL (optional)", |
| #4236 | }, |
| #4237 | "telegram": { |
| #4238 | "type": "string", |
| #4239 | "description": "Telegram URL (optional)", |
| #4240 | }, |
| #4241 | "website": { |
| #4242 | "type": "string", |
| #4243 | "description": "Website URL (optional)", |
| #4244 | }, |
| #4245 | "initial_buy_sol": { |
| #4246 | "type": "number", |
| #4247 | "description": "Initial buy amount in SOL (default: 0)", |
| #4248 | }, |
| #4249 | }, |
| #4250 | "required": ["name", "symbol", "description"], |
| #4251 | } |
| #4252 | |
| #4253 | async def execute( |
| #4254 | self, |
| #4255 | name: str, |
| #4256 | symbol: str, |
| #4257 | description: str, |
| #4258 | image_url: Optional[str] = None, |
| #4259 | image_path: Optional[str] = None, |
| #4260 | twitter: Optional[str] = None, |
| #4261 | telegram: Optional[str] = None, |
| #4262 | website: Optional[str] = None, |
| #4263 | initial_buy_sol: float = 0.0, |
| #4264 | ) -> ToolResult: |
| #4265 | try: |
| #4266 | pumpfun = get_pumpfun_client() |
| #4267 | if pumpfun is None: |
| #4268 | return ToolResult( |
| #4269 | success=False, |
| #4270 | error="PumpFun client not configured. Set PRIVATE_KEY and RPC_URL." |
| #4271 | ) |
| #4272 | |
| #4273 | result = await pumpfun.create_token( |
| #4274 | name=name, |
| #4275 | symbol=symbol, |
| #4276 | description=description, |
| #4277 | image_url=image_url, |
| #4278 | image_path=image_path, |
| #4279 | twitter=twitter, |
| #4280 | telegram=telegram, |
| #4281 | website=website, |
| #4282 | initial_buy_sol=initial_buy_sol, |
| #4283 | ) |
| #4284 | |
| #4285 | return ToolResult( |
| #4286 | success=True, |
| #4287 | content=json.dumps({ |
| #4288 | "status": "success", |
| #4289 | "mint": str(result.mint), |
| #4290 | "bonding_curve": str(result.bonding_curve), |
| #4291 | "signature": result.signature, |
| #4292 | "token_url": result.token_url, |
| #4293 | "message": f"Token {name} ({symbol}) launched on pump.fun! View at {result.token_url}" |
| #4294 | }, indent=2) |
| #4295 | ) |
| #4296 | except Exception as e: |
| #4297 | return ToolResult(success=False, error=f"Failed to launch token: {str(e)}") |
| #4298 | |
| #4299 | |
| #4300 | class PumpFunBuyTool(Tool): |
| #4301 | """Tool to buy tokens from a pump.fun bonding curve.""" |
| #4302 | |
| #4303 | @property |
| #4304 | def name(self) -> str: |
| #4305 | return "pumpfun_buy" |
| #4306 | |
| #4307 | @property |
| #4308 | def description(self) -> str: |
| #4309 | return """Buy tokens from a pump.fun bonding curve using SOL. |
| #4310 | |
| #4311 | Only works for tokens still on the bonding curve (not yet migrated to Raydium). |
| #4312 | Uses constant product AMM formula for pricing. |
| #4313 | |
| #4314 | Examples: |
| #4315 | - "Buy 0.1 SOL worth of token ABC123..." |
| #4316 | - "Purchase pump.fun token with mint address..." |
| #4317 | """ |
| #4318 | |
| #4319 | @property |
| #4320 | def parameters(self) -> dict[str, Any]: |
| #4321 | return { |
| #4322 | "type": "object", |
| #4323 | "properties": { |
| #4324 | "mint": { |
| #4325 | "type": "string", |
| #4326 | "description": "Token mint address", |
| #4327 | }, |
| #4328 | "sol_amount": { |
| #4329 | "type": "number", |
| #4330 | "description": "Amount of SOL to spend", |
| #4331 | }, |
| #4332 | "slippage_bps": { |
| #4333 | "type": "integer", |
| #4334 | "description": "Slippage tolerance in basis points (default: 500 = 5%)", |
| #4335 | }, |
| #4336 | }, |
| #4337 | "required": ["mint", "sol_amount"], |
| #4338 | } |
| #4339 | |
| #4340 | async def execute( |
| #4341 | self, |
| #4342 | mint: str, |
| #4343 | sol_amount: float, |
| #4344 | slippage_bps: int = 500, |
| #4345 | ) -> ToolResult: |
| #4346 | try: |
| #4347 | pumpfun = get_pumpfun_client() |
| #4348 | if pumpfun is None: |
| #4349 | return ToolResult( |
| #4350 | success=False, |
| #4351 | error="PumpFun client not configured. Set PRIVATE_KEY and RPC_URL." |
| #4352 | ) |
| #4353 | |
| #4354 | from solders.pubkey import Pubkey |
| #4355 | mint_pubkey = Pubkey.from_string(mint) |
| #4356 | |
| #4357 | signature = await pumpfun.buy( |
| #4358 | mint=mint_pubkey, |
| #4359 | sol_amount=sol_amount, |
| #4360 | slippage_bps=slippage_bps, |
| #4361 | ) |
| #4362 | |
| #4363 | return ToolResult( |
| #4364 | success=True, |
| #4365 | content=json.dumps({ |
| #4366 | "status": "success", |
| #4367 | "action": "buy", |
| #4368 | "mint": mint, |
| #4369 | "sol_amount": sol_amount, |
| #4370 | "signature": signature, |
| #4371 | "message": f"Bought tokens with {sol_amount} SOL" |
| #4372 | }, indent=2) |
| #4373 | ) |
| #4374 | except Exception as e: |
| #4375 | return ToolResult(success=False, error=f"Failed to buy: {str(e)}") |
| #4376 | |
| #4377 | |
| #4378 | class PumpFunSellTool(Tool): |
| #4379 | """Tool to sell tokens to a pump.fun bonding curve.""" |
| #4380 | |
| #4381 | @property |
| #4382 | def name(self) -> str: |
| #4383 | return "pumpfun_sell" |
| #4384 | |
| #4385 | @property |
| #4386 | def description(self) -> str: |
| #4387 | return """Sell tokens to a pump.fun bonding curve for SOL. |
| #4388 | |
| #4389 | Only works for tokens still on the bonding curve (not yet migrated to Raydium). |
| #4390 | |
| #4391 | Examples: |
| #4392 | - "Sell 1000000 tokens of ABC123..." |
| #4393 | - "Dump my pump.fun tokens" |
| #4394 | """ |
| #4395 | |
| #4396 | @property |
| #4397 | def parameters(self) -> dict[str, Any]: |
| #4398 | return { |
| #4399 | "type": "object", |
| #4400 | "properties": { |
| #4401 | "mint": { |
| #4402 | "type": "string", |
| #4403 | "description": "Token mint address", |
| #4404 | }, |
| #4405 | "token_amount": { |
| #4406 | "type": "integer", |
| #4407 | "description": "Amount of tokens to sell (in smallest unit)", |
| #4408 | }, |
| #4409 | "slippage_bps": { |
| #4410 | "type": "integer", |
| #4411 | "description": "Slippage tolerance in basis points (default: 500 = 5%)", |
| #4412 | }, |
| #4413 | }, |
| #4414 | "required": ["mint", "token_amount"], |
| #4415 | } |
| #4416 | |
| #4417 | async def execute( |
| #4418 | self, |
| #4419 | mint: str, |
| #4420 | token_amount: int, |
| #4421 | slippage_bps: int = 500, |
| #4422 | ) -> ToolResult: |
| #4423 | try: |
| #4424 | pumpfun = get_pumpfun_client() |
| #4425 | if pumpfun is None: |
| #4426 | return ToolResult( |
| #4427 | success=False, |
| #4428 | error="PumpFun client not configured. Set PRIVATE_KEY and RPC_URL." |
| #4429 | ) |
| #4430 | |
| #4431 | from solders.pubkey import Pubkey |
| #4432 | mint_pubkey = Pubkey.from_string(mint) |
| #4433 | |
| #4434 | signature = await pumpfun.sell( |
| #4435 | mint=mint_pubkey, |
| #4436 | token_amount=token_amount, |
| #4437 | slippage_bps=slippage_bps, |
| #4438 | ) |
| #4439 | |
| #4440 | return ToolResult( |
| #4441 | success=True, |
| #4442 | content=json.dumps({ |
| #4443 | "status": "success", |
| #4444 | "action": "sell", |
| #4445 | "mint": mint, |
| #4446 | "token_amount": token_amount, |
| #4447 | "signature": signature, |
| #4448 | "message": f"Sold {token_amount} tokens" |
| #4449 | }, indent=2) |
| #4450 | ) |
| #4451 | except Exception as e: |
| #4452 | return ToolResult(success=False, error=f"Failed to sell: {str(e)}") |
| #4453 | |
| #4454 | |
| #4455 | class PumpFunGetPriceTool(Tool): |
| #4456 | """Tool to get current price and stats for a pump.fun token.""" |
| #4457 | |
| #4458 | @property |
| #4459 | def name(self) -> str: |
| #4460 | return "pumpfun_get_price" |
| #4461 | |
| #4462 | @property |
| #4463 | def description(self) -> str: |
| #4464 | return """Get current price and bonding curve stats for a pump.fun token. |
| #4465 | |
| #4466 | Returns price, market cap, progress to graduation (Raydium migration). |
| #4467 | Only works for tokens on pump.fun bonding curve. |
| #4468 | |
| #4469 | Examples: |
| #4470 | - "Get price of pump.fun token ABC123..." |
| #4471 | - "Check pump.fun bonding curve status" |
| #4472 | """ |
| #4473 | |
| #4474 | @property |
| #4475 | def parameters(self) -> dict[str, Any]: |
| #4476 | return { |
| #4477 | "type": "object", |
| #4478 | "properties": { |
| #4479 | "mint": { |
| #4480 | "type": "string", |
| #4481 | "description": "Token mint address", |
| #4482 | }, |
| #4483 | }, |
| #4484 | "required": ["mint"], |
| #4485 | } |
| #4486 | |
| #4487 | async def execute(self, mint: str) -> ToolResult: |
| #4488 | try: |
| #4489 | pumpfun = get_pumpfun_client() |
| #4490 | if pumpfun is None: |
| #4491 | return ToolResult( |
| #4492 | success=False, |
| #4493 | error="PumpFun client not configured. Set PRIVATE_KEY and RPC_URL." |
| #4494 | ) |
| #4495 | |
| #4496 | from solders.pubkey import Pubkey |
| #4497 | mint_pubkey = Pubkey.from_string(mint) |
| #4498 | |
| #4499 | price_data = await pumpfun.get_token_price(mint_pubkey) |
| #4500 | |
| #4501 | if price_data is None: |
| #4502 | return ToolResult( |
| #4503 | success=False, |
| #4504 | error="Token not found on pump.fun bonding curve" |
| #4505 | ) |
| #4506 | |
| #4507 | # Format output |
| #4508 | content = f"🎢 Pump.fun Token Stats\n\n" |
| #4509 | content += f"💰 Price: {price_data['price_per_token_sol']:.10f} SOL\n" |
| #4510 | content += f"📊 Market Cap: {price_data['market_cap_sol']:.2f} SOL\n" |
| #4511 | content += f"📈 Progress: {price_data['progress_percent']:.2f}%\n" |
| #4512 | content += f"💧 Virtual SOL: {price_data['virtual_sol_reserves']:.4f}\n" |
| #4513 | content += f"🪙 Real Token Reserves: {price_data['real_token_reserves']:,}\n" |
| #4514 | |
| #4515 | if price_data['complete']: |
| #4516 | content += "\n✅ Bonding curve COMPLETE - Token migrated to Raydium" |
| #4517 | else: |
| #4518 | content += f"\n⏳ {100 - price_data['progress_percent']:.2f}% to graduation" |
| #4519 | |
| #4520 | return ToolResult( |
| #4521 | success=True, |
| #4522 | content=content |
| #4523 | ) |
| #4524 | except Exception as e: |
| #4525 | return ToolResult(success=False, error=f"Failed to get price: {str(e)}") |
| #4526 | |
| #4527 | |
| #4528 | # Export all tools |
| #4529 | ALL_SOLANA_TOOLS = [ |
| #4530 | GetWalletBalanceTool, |
| #4531 | GetTokenPriceTool, |
| #4532 | GetTokenInfoTool, |
| #4533 | GetSwapQuoteTool, |
| #4534 | BuyTokenTool, |
| #4535 | SellTokenTool, |
| #4536 | GetPortfolioTool, |
| #4537 | GetTrendingTokensTool, |
| #4538 | SearchTokenTool, |
| #4539 | GetWalletNetWorthTool, |
| #4540 | GetWalletNetWorthChartTool, |
| #4541 | GetWalletPnLTool, |
| #4542 | GetTokenChartTool, |
| #4543 | AnalyzeTokenSecurityTool, |
| #4544 | LaunchTokenTool, |
| #4545 | PostToTwitterTool, |
| #4546 | GenerateImageTool, |
| #4547 | GenerateMusicTool, |
| #4548 | GenerateVideoTool, |
| #4549 | TextToSpeechTool, |
| #4550 | WebSearchTool, |
| #4551 | AnalyzeSolanaAddressTool, |
| #4552 | # Aster DEX Tools |
| #4553 | AsterOpenLongTool, |
| #4554 | AsterOpenShortTool, |
| #4555 | AsterClosePerpPositionTool, |
| #4556 | AsterGetPositionsTool, |
| #4557 | AsterSetLeverageTool, |
| #4558 | AsterSpotBuyTool, |
| #4559 | AsterSpotSellTool, |
| #4560 | AsterGetBalanceTool, |
| #4561 | AsterTransferTool, |
| #4562 | AsterGetPriceTool, |
| #4563 | # Hyperliquid DEX Tools |
| #4564 | HyperliquidGetAccountTool, |
| #4565 | HyperliquidGetPriceTool, |
| #4566 | HyperliquidOpenLongTool, |
| #4567 | HyperliquidOpenShortTool, |
| #4568 | HyperliquidClosePositionTool, |
| #4569 | HyperliquidGetPositionsTool, |
| #4570 | HyperliquidSetLeverageTool, |
| #4571 | HyperliquidPlaceLimitOrderTool, |
| #4572 | HyperliquidCancelOrderTool, |
| #4573 | HyperliquidGetOpenOrdersTool, |
| #4574 | HyperliquidGetTradeHistoryTool, |
| #4575 | HyperliquidGetAvailableCoinsTool, |
| #4576 | HyperliquidTransferTool, |
| #4577 | HyperliquidGetAssetIdsTool, |
| #4578 | HyperliquidSpotOrderTool, |
| #4579 | # CDP Tools |
| #4580 | CDPCreateAccountTool, |
| #4581 | CDPRequestFaucetTool, |
| #4582 | CDPGetBalanceTool, |
| #4583 | CDPSendSOLTool, |
| #4584 | CDPListAccountsTool, |
| #4585 | # CoinGecko Tools |
| #4586 | GetCryptoPriceTool, |
| #4587 | GetCoinMarketDataTool, |
| #4588 | GetTrendingCryptosTool, |
| #4589 | SearchCryptoTool, |
| #4590 | GetGlobalCryptoStatsTool, |
| #4591 | # PumpFun Tools |
| #4592 | PumpFunLaunchTokenTool, |
| #4593 | PumpFunBuyTool, |
| #4594 | PumpFunSellTool, |
| #4595 | PumpFunGetPriceTool, |
| #4596 | ] |
| #4597 | |
| #4598 | |
| #4599 | def create_all_tools() -> list[Tool]: |
| #4600 | """Create instances of all Solana trading tools.""" |
| #4601 | return [ToolCls() for ToolCls in ALL_SOLANA_TOOLS] |
| #4602 |