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 | """Birdeye API Client for Token Price and Market Data""" |
| #2 | |
| #3 | import httpx |
| #4 | import websockets |
| #5 | import json |
| #6 | import asyncio |
| #7 | from typing import Optional, Any, Callable, Dict, List |
| #8 | from dataclasses import dataclass |
| #9 | from datetime import datetime |
| #10 | from enum import Enum |
| #11 | |
| #12 | |
| #13 | BIRDEYE_API_BASE_URL = "https://public-api.birdeye.so" |
| #14 | BIRDEYE_WSS_URL = "wss://public-api.birdeye.so/socket/solana" |
| #15 | |
| #16 | |
| #17 | class BirdeyeWSEvent(str, Enum): |
| #18 | """Birdeye WebSocket event types""" |
| #19 | SUBSCRIBE_PRICE = "SUBSCRIBE_PRICE" |
| #20 | SUBSCRIBE_BASE_QUOTE_PRICE = "SUBSCRIBE_BASE_QUOTE_PRICE" |
| #21 | SUBSCRIBE_TOKEN_NEW_LISTING = "SUBSCRIBE_TOKEN_NEW_LISTING" |
| #22 | SUBSCRIBE_NEW_PAIR = "SUBSCRIBE_NEW_PAIR" |
| #23 | SUBSCRIBE_LARGE_TRADE_TXS = "SUBSCRIBE_LARGE_TRADE_TXS" |
| #24 | SUBSCRIBE_WALLET_TXS = "SUBSCRIBE_WALLET_TXS" |
| #25 | SUBSCRIBE_TOKEN_STATS = "SUBSCRIBE_TOKEN_STATS" |
| #26 | |
| #27 | |
| #28 | @dataclass |
| #29 | class TokenPrice: |
| #30 | """Token price info""" |
| #31 | mint: str |
| #32 | symbol: str |
| #33 | name: str |
| #34 | price: float |
| #35 | price_change_24h: float |
| #36 | volume_24h: float |
| #37 | liquidity: float |
| #38 | timestamp: datetime |
| #39 | |
| #40 | |
| #41 | @dataclass |
| #42 | class TokenOverview: |
| #43 | """Comprehensive token overview""" |
| #44 | mint: str |
| #45 | symbol: str |
| #46 | name: str |
| #47 | decimals: int |
| #48 | price: float |
| #49 | price_change_24h: float |
| #50 | price_change_1h: float |
| #51 | volume_24h: float |
| #52 | volume_change_24h: float |
| #53 | liquidity: float |
| #54 | market_cap: float |
| #55 | supply: float |
| #56 | holder_count: int |
| #57 | extensions: dict |
| #58 | |
| #59 | |
| #60 | @dataclass |
| #61 | class OHLCVData: |
| #62 | """OHLCV candle data""" |
| #63 | timestamp: int |
| #64 | open: float |
| #65 | high: float |
| #66 | low: float |
| #67 | close: float |
| #68 | volume: float |
| #69 | |
| #70 | |
| #71 | class BirdeyeClient: |
| #72 | """Client for Birdeye Token Data API""" |
| #73 | |
| #74 | def __init__( |
| #75 | self, |
| #76 | api_key: str, |
| #77 | base_url: str = BIRDEYE_API_BASE_URL, |
| #78 | chain: str = "solana", |
| #79 | ): |
| #80 | """ |
| #81 | Initialize Birdeye client. |
| #82 | |
| #83 | Args: |
| #84 | api_key: Birdeye API key |
| #85 | base_url: API base URL |
| #86 | chain: Blockchain (default: solana) |
| #87 | """ |
| #88 | self.api_key = api_key |
| #89 | self.base_url = base_url |
| #90 | self.chain = chain |
| #91 | |
| #92 | self._client = httpx.AsyncClient( |
| #93 | base_url=base_url, |
| #94 | headers={ |
| #95 | "X-API-KEY": api_key, |
| #96 | "x-chain": chain, |
| #97 | }, |
| #98 | timeout=30.0 |
| #99 | ) |
| #100 | |
| #101 | async def _get(self, endpoint: str, params: dict = None) -> Any: |
| #102 | """Make GET request to Birdeye API""" |
| #103 | response = await self._client.get(endpoint, params=params) |
| #104 | response.raise_for_status() |
| #105 | data = response.json() |
| #106 | |
| #107 | if not data.get("success", True): |
| #108 | raise Exception(f"Birdeye API error: {data.get('message', 'Unknown error')}") |
| #109 | |
| #110 | return data.get("data", data) |
| #111 | |
| #112 | # ===================== |
| #113 | # Price Methods |
| #114 | # ===================== |
| #115 | |
| #116 | async def get_token_price(self, mint: str) -> dict: |
| #117 | """ |
| #118 | Get current price for a token. |
| #119 | |
| #120 | Args: |
| #121 | mint: Token mint address |
| #122 | |
| #123 | Returns: |
| #124 | Price data |
| #125 | """ |
| #126 | return await self._get("/defi/price", params={"address": mint}) |
| #127 | |
| #128 | async def get_multi_token_prices(self, mints: list[str]) -> dict: |
| #129 | """ |
| #130 | Get prices for multiple tokens. |
| #131 | |
| #132 | Args: |
| #133 | mints: List of token mint addresses |
| #134 | |
| #135 | Returns: |
| #136 | Dict of mint -> price data |
| #137 | """ |
| #138 | addresses = ",".join(mints) |
| #139 | return await self._get("/defi/multi_price", params={"list_address": addresses}) |
| #140 | |
| #141 | async def get_token_price_history( |
| #142 | self, |
| #143 | mint: str, |
| #144 | address_type: str = "token", |
| #145 | time_type: str = "24h", |
| #146 | ) -> list[dict]: |
| #147 | """ |
| #148 | Get price history for a token. |
| #149 | |
| #150 | Args: |
| #151 | mint: Token mint address |
| #152 | address_type: 'token' or 'pair' |
| #153 | time_type: '24h', '7d', '30d', '1y' |
| #154 | |
| #155 | Returns: |
| #156 | List of price points |
| #157 | """ |
| #158 | return await self._get( |
| #159 | "/defi/history_price", |
| #160 | params={ |
| #161 | "address": mint, |
| #162 | "address_type": address_type, |
| #163 | "type": time_type, |
| #164 | } |
| #165 | ) |
| #166 | |
| #167 | # ===================== |
| #168 | # Token Info Methods |
| #169 | # ===================== |
| #170 | |
| #171 | async def get_token_overview(self, mint: str) -> TokenOverview: |
| #172 | """ |
| #173 | Get comprehensive token overview. |
| #174 | |
| #175 | Args: |
| #176 | mint: Token mint address |
| #177 | |
| #178 | Returns: |
| #179 | TokenOverview with all token data |
| #180 | """ |
| #181 | data = await self._get("/defi/token_overview", params={"address": mint}) |
| #182 | |
| #183 | return TokenOverview( |
| #184 | mint=mint, |
| #185 | symbol=data.get("symbol", ""), |
| #186 | name=data.get("name", ""), |
| #187 | decimals=data.get("decimals", 0), |
| #188 | price=float(data.get("price", 0)), |
| #189 | price_change_24h=float(data.get("priceChange24h", 0)), |
| #190 | price_change_1h=float(data.get("priceChange1h", 0)), |
| #191 | volume_24h=float(data.get("v24h", 0)), |
| #192 | volume_change_24h=float(data.get("v24hChangePercent", 0)), |
| #193 | liquidity=float(data.get("liquidity", 0)), |
| #194 | market_cap=float(data.get("mc", 0)), |
| #195 | supply=float(data.get("supply", 0)), |
| #196 | holder_count=int(data.get("holder", 0)), |
| #197 | extensions=data.get("extensions", {}), |
| #198 | ) |
| #199 | |
| #200 | async def get_token_security(self, mint: str) -> dict: |
| #201 | """ |
| #202 | Get token security info (ownership, mintability, etc.). |
| #203 | |
| #204 | Args: |
| #205 | mint: Token mint address |
| #206 | |
| #207 | Returns: |
| #208 | Security info |
| #209 | """ |
| #210 | return await self._get("/defi/token_security", params={"address": mint}) |
| #211 | |
| #212 | async def get_token_creation_info(self, mint: str) -> dict: |
| #213 | """ |
| #214 | Get token creation information. |
| #215 | |
| #216 | Args: |
| #217 | mint: Token mint address |
| #218 | |
| #219 | Returns: |
| #220 | Creation info including creator, timestamp, etc. |
| #221 | """ |
| #222 | return await self._get("/defi/token_creation_info", params={"address": mint}) |
| #223 | |
| #224 | async def get_token_metadata(self, mint: str) -> dict: |
| #225 | """ |
| #226 | Get token metadata. |
| #227 | |
| #228 | Args: |
| #229 | mint: Token mint address |
| #230 | |
| #231 | Returns: |
| #232 | Token metadata |
| #233 | """ |
| #234 | return await self._get("/defi/v3/token/meta-data/single", params={"address": mint}) |
| #235 | |
| #236 | # ===================== |
| #237 | # Market Data Methods |
| #238 | # ===================== |
| #239 | |
| #240 | async def get_ohlcv( |
| #241 | self, |
| #242 | mint: str, |
| #243 | time_type: str = "15m", |
| #244 | time_from: Optional[int] = None, |
| #245 | time_to: Optional[int] = None, |
| #246 | ) -> list[OHLCVData]: |
| #247 | """ |
| #248 | Get OHLCV (candlestick) data. |
| #249 | |
| #250 | Args: |
| #251 | mint: Token mint address |
| #252 | time_type: Candle interval ('1m', '3m', '5m', '15m', '30m', '1H', '2H', '4H', '6H', '8H', '12H', '1D', '3D', '1W', '1M') |
| #253 | time_from: Start timestamp (unix) |
| #254 | time_to: End timestamp (unix) |
| #255 | |
| #256 | Returns: |
| #257 | List of OHLCV candles |
| #258 | """ |
| #259 | params = { |
| #260 | "address": mint, |
| #261 | "type": time_type, |
| #262 | } |
| #263 | if time_from: |
| #264 | params["time_from"] = time_from |
| #265 | if time_to: |
| #266 | params["time_to"] = time_to |
| #267 | |
| #268 | data = await self._get("/defi/ohlcv", params=params) |
| #269 | items = data.get("items", []) if isinstance(data, dict) else data |
| #270 | |
| #271 | return [ |
| #272 | OHLCVData( |
| #273 | timestamp=item.get("unixTime", 0), |
| #274 | open=float(item.get("o", 0)), |
| #275 | high=float(item.get("h", 0)), |
| #276 | low=float(item.get("l", 0)), |
| #277 | close=float(item.get("c", 0)), |
| #278 | volume=float(item.get("v", 0)), |
| #279 | ) |
| #280 | for item in items |
| #281 | ] |
| #282 | |
| #283 | async def get_trades( |
| #284 | self, |
| #285 | mint: str, |
| #286 | trade_type: str = "swap", |
| #287 | limit: int = 50, |
| #288 | offset: int = 0, |
| #289 | ) -> list[dict]: |
| #290 | """ |
| #291 | Get recent trades for a token. |
| #292 | |
| #293 | Args: |
| #294 | mint: Token mint address |
| #295 | trade_type: 'swap' or 'all' |
| #296 | limit: Max trades to return |
| #297 | offset: Pagination offset |
| #298 | |
| #299 | Returns: |
| #300 | List of trades |
| #301 | """ |
| #302 | return await self._get( |
| #303 | "/defi/txs/token", |
| #304 | params={ |
| #305 | "address": mint, |
| #306 | "tx_type": trade_type, |
| #307 | "limit": limit, |
| #308 | "offset": offset, |
| #309 | } |
| #310 | ) |
| #311 | |
| #312 | async def get_pair_trades( |
| #313 | self, |
| #314 | pair_address: str, |
| #315 | trade_type: str = "swap", |
| #316 | limit: int = 50, |
| #317 | offset: int = 0, |
| #318 | ) -> list[dict]: |
| #319 | """ |
| #320 | Get recent trades for a liquidity pair. |
| #321 | |
| #322 | Args: |
| #323 | pair_address: Pair/pool address |
| #324 | trade_type: 'swap' or 'all' |
| #325 | limit: Max trades to return |
| #326 | offset: Pagination offset |
| #327 | |
| #328 | Returns: |
| #329 | List of trades |
| #330 | """ |
| #331 | return await self._get( |
| #332 | "/defi/txs/pair", |
| #333 | params={ |
| #334 | "address": pair_address, |
| #335 | "tx_type": trade_type, |
| #336 | "limit": limit, |
| #337 | "offset": offset, |
| #338 | } |
| #339 | ) |
| #340 | |
| #341 | # ===================== |
| #342 | # Wallet Methods |
| #343 | # ===================== |
| #344 | |
| #345 | async def get_wallet_portfolio(self, wallet: str) -> dict: |
| #346 | """ |
| #347 | Get wallet portfolio with all token holdings. |
| #348 | |
| #349 | Args: |
| #350 | wallet: Wallet address |
| #351 | |
| #352 | Returns: |
| #353 | Portfolio data |
| #354 | """ |
| #355 | return await self._get("/v1/wallet/token_list", params={"wallet": wallet}) |
| #356 | |
| #357 | async def get_wallet_token_balance(self, wallet: str, mint: str) -> dict: |
| #358 | """ |
| #359 | Get specific token balance for a wallet. |
| #360 | |
| #361 | Args: |
| #362 | wallet: Wallet address |
| #363 | mint: Token mint address |
| #364 | |
| #365 | Returns: |
| #366 | Token balance info |
| #367 | """ |
| #368 | return await self._get( |
| #369 | "/v1/wallet/token_balance", |
| #370 | params={"wallet": wallet, "token_address": mint} |
| #371 | ) |
| #372 | |
| #373 | async def get_wallet_transactions( |
| #374 | self, |
| #375 | wallet: str, |
| #376 | limit: int = 50, |
| #377 | before_time: Optional[int] = None, |
| #378 | ) -> list[dict]: |
| #379 | """ |
| #380 | Get wallet transaction history. |
| #381 | |
| #382 | Args: |
| #383 | wallet: Wallet address |
| #384 | limit: Max transactions |
| #385 | before_time: Get transactions before this timestamp |
| #386 | |
| #387 | Returns: |
| #388 | List of transactions |
| #389 | """ |
| #390 | params = {"wallet": wallet, "limit": limit} |
| #391 | if before_time: |
| #392 | params["before_time"] = before_time |
| #393 | |
| #394 | return await self._get("/v1/wallet/tx_list", params=params) |
| #395 | |
| #396 | # ===================== |
| #397 | # Wallet Net Worth & PnL Methods |
| #398 | # ===================== |
| #399 | |
| #400 | async def get_wallet_net_worth( |
| #401 | self, |
| #402 | wallet: str, |
| #403 | filter_value: Optional[float] = None, |
| #404 | sort_by: str = "value", |
| #405 | sort_type: str = "desc", |
| #406 | limit: int = 100, |
| #407 | offset: int = 0, |
| #408 | ) -> dict: |
| #409 | """ |
| #410 | Get current net worth and portfolio of a wallet. |
| #411 | |
| #412 | Args: |
| #413 | wallet: Wallet address |
| #414 | filter_value: Filter assets >= this value |
| #415 | sort_by: Sort field (default: 'value') |
| #416 | sort_type: 'desc' or 'asc' |
| #417 | limit: Max assets to return |
| #418 | offset: Pagination offset |
| #419 | |
| #420 | Returns: |
| #421 | Net worth data with asset list |
| #422 | """ |
| #423 | params = { |
| #424 | "wallet": wallet, |
| #425 | "sort_by": sort_by, |
| #426 | "sort_type": sort_type, |
| #427 | "limit": limit, |
| #428 | "offset": offset, |
| #429 | } |
| #430 | if filter_value is not None: |
| #431 | params["filter_value"] = filter_value |
| #432 | |
| #433 | return await self._get("/wallet/v2/current-net-worth", params=params) |
| #434 | |
| #435 | async def get_wallet_net_worth_chart( |
| #436 | self, |
| #437 | wallet: str, |
| #438 | count: int = 7, |
| #439 | direction: str = "back", |
| #440 | time: Optional[str] = None, |
| #441 | time_type: str = "1d", |
| #442 | sort_type: str = "desc", |
| #443 | ) -> dict: |
| #444 | """ |
| #445 | Get historical net worth chart data. |
| #446 | |
| #447 | Args: |
| #448 | wallet: Wallet address |
| #449 | count: Number of intervals (1-30) |
| #450 | direction: 'back' or 'forward' |
| #451 | time: Base timestamp (ISO 8601 UTC, e.g., '2025-07-31 23:59:59') |
| #452 | time_type: '1h' or '1d' |
| #453 | sort_type: 'desc' or 'asc' |
| #454 | |
| #455 | Returns: |
| #456 | Historical net worth data |
| #457 | """ |
| #458 | params = { |
| #459 | "wallet": wallet, |
| #460 | "count": count, |
| #461 | "direction": direction, |
| #462 | "type": time_type, |
| #463 | "sort_type": sort_type, |
| #464 | } |
| #465 | if time: |
| #466 | params["time"] = time |
| #467 | |
| #468 | return await self._get("/wallet/v2/net-worth", params=params) |
| #469 | |
| #470 | async def get_wallet_net_worth_details( |
| #471 | self, |
| #472 | wallet: str, |
| #473 | time: Optional[str] = None, |
| #474 | time_type: str = "1d", |
| #475 | sort_type: str = "desc", |
| #476 | limit: int = 100, |
| #477 | offset: int = 0, |
| #478 | ) -> dict: |
| #479 | """ |
| #480 | Get asset details of a wallet at a specific time. |
| #481 | |
| #482 | Args: |
| #483 | wallet: Wallet address |
| #484 | time: Timestamp (ISO 8601 UTC) |
| #485 | time_type: '1h' or '1d' (time must be within 7 days for '1h') |
| #486 | sort_type: 'desc' or 'asc' |
| #487 | limit: Max assets |
| #488 | offset: Pagination offset |
| #489 | |
| #490 | Returns: |
| #491 | Asset details at specified time |
| #492 | """ |
| #493 | params = { |
| #494 | "wallet": wallet, |
| #495 | "type": time_type, |
| #496 | "sort_type": sort_type, |
| #497 | "limit": limit, |
| #498 | "offset": offset, |
| #499 | } |
| #500 | if time: |
| #501 | params["time"] = time |
| #502 | |
| #503 | return await self._get("/wallet/v2/net-worth-details", params=params) |
| #504 | |
| #505 | async def get_wallet_pnl_summary( |
| #506 | self, |
| #507 | wallet: str, |
| #508 | duration: str = "all", |
| #509 | ) -> dict: |
| #510 | """ |
| #511 | Get PnL (Profit & Loss) summary for a wallet. |
| #512 | |
| #513 | Args: |
| #514 | wallet: Wallet address |
| #515 | duration: 'all', '90d', '30d', '7d', '24h' |
| #516 | |
| #517 | Returns: |
| #518 | PnL summary with realized/unrealized profit, trade counts, win rate |
| #519 | """ |
| #520 | return await self._get( |
| #521 | "/wallet/v2/pnl/summary", |
| #522 | params={"wallet": wallet, "duration": duration} |
| #523 | ) |
| #524 | |
| #525 | async def _post(self, endpoint: str, json_data: dict = None) -> Any: |
| #526 | """Make POST request to Birdeye API""" |
| #527 | response = await self._client.post(endpoint, json=json_data) |
| #528 | response.raise_for_status() |
| #529 | data = response.json() |
| #530 | |
| #531 | if not data.get("success", True): |
| #532 | raise Exception(f"Birdeye API error: {data.get('message', 'Unknown error')}") |
| #533 | |
| #534 | return data.get("data", data) |
| #535 | |
| #536 | async def get_wallet_pnl_details( |
| #537 | self, |
| #538 | wallet: str, |
| #539 | token_addresses: Optional[list[str]] = None, |
| #540 | sort_by: str = "value", |
| #541 | sort_type: str = "desc", |
| #542 | limit: int = 100, |
| #543 | offset: int = 0, |
| #544 | ) -> dict: |
| #545 | """ |
| #546 | Get detailed PnL broken down by token. |
| #547 | |
| #548 | Args: |
| #549 | wallet: Wallet address |
| #550 | token_addresses: Optional list of specific tokens (max 100) |
| #551 | sort_by: 'value' or 'last_trade' |
| #552 | sort_type: 'desc' or 'asc' |
| #553 | limit: Max tokens |
| #554 | offset: Pagination offset |
| #555 | |
| #556 | Returns: |
| #557 | Detailed PnL per token |
| #558 | """ |
| #559 | body = { |
| #560 | "wallet": wallet, |
| #561 | "sort_by": sort_by, |
| #562 | "sort_type": sort_type, |
| #563 | "limit": limit, |
| #564 | "offset": offset, |
| #565 | } |
| #566 | if token_addresses: |
| #567 | body["token_addresses"] = token_addresses |
| #568 | |
| #569 | return await self._post("/wallet/v2/pnl/details", json_data=body) |
| #570 | |
| #571 | # ===================== |
| #572 | # OHLCV & Price Chart Methods (Additional) |
| #573 | # ===================== |
| #574 | |
| #575 | async def get_ohlcv_pair( |
| #576 | self, |
| #577 | address: str, |
| #578 | type: str = "15m", |
| #579 | time_from: int = None, |
| #580 | time_to: int = None, |
| #581 | ) -> dict: |
| #582 | """ |
| #583 | Get OHLCV candlestick data for a trading pair. |
| #584 | |
| #585 | Args: |
| #586 | address: Pair address |
| #587 | type: Time frame |
| #588 | time_from: Unix timestamp in seconds |
| #589 | time_to: Unix timestamp in seconds |
| #590 | |
| #591 | Returns: |
| #592 | OHLCV data for the pair |
| #593 | """ |
| #594 | import time |
| #595 | |
| #596 | if time_to is None: |
| #597 | time_to = int(time.time()) |
| #598 | if time_from is None: |
| #599 | time_from = time_to - 86400 |
| #600 | |
| #601 | params = { |
| #602 | "address": address, |
| #603 | "type": type, |
| #604 | "time_from": time_from, |
| #605 | "time_to": time_to, |
| #606 | } |
| #607 | |
| #608 | return await self._get("/defi/ohlcv/pair", params=params) |
| #609 | |
| #610 | async def get_ohlcv_base_quote( |
| #611 | self, |
| #612 | base_address: str, |
| #613 | quote_address: str = "So11111111111111111111111111111111111111112", # SOL |
| #614 | type: str = "15m", |
| #615 | time_from: int = None, |
| #616 | time_to: int = None, |
| #617 | ) -> dict: |
| #618 | """ |
| #619 | Get OHLCV data for a base-quote pair. |
| #620 | |
| #621 | Args: |
| #622 | base_address: Base token address |
| #623 | quote_address: Quote token address (default: SOL) |
| #624 | type: Time frame |
| #625 | time_from: Unix timestamp in seconds |
| #626 | time_to: Unix timestamp in seconds |
| #627 | |
| #628 | Returns: |
| #629 | OHLCV data for base/quote pair |
| #630 | """ |
| #631 | import time |
| #632 | |
| #633 | if time_to is None: |
| #634 | time_to = int(time.time()) |
| #635 | if time_from is None: |
| #636 | time_from = time_to - 86400 |
| #637 | |
| #638 | params = { |
| #639 | "base_address": base_address, |
| #640 | "quote_address": quote_address, |
| #641 | "type": type, |
| #642 | "time_from": time_from, |
| #643 | "time_to": time_to, |
| #644 | } |
| #645 | |
| #646 | return await self._get("/defi/ohlcv/base_quote", params=params) |
| #647 | |
| #648 | # ===================== |
| #649 | # Search & Discovery Methods |
| #650 | # ===================== |
| #651 | |
| #652 | async def search_token(self, query: str, limit: int = 20) -> list[dict]: |
| #653 | """ |
| #654 | Search for tokens by name or symbol. |
| #655 | |
| #656 | Args: |
| #657 | query: Search query |
| #658 | limit: Max results |
| #659 | |
| #660 | Returns: |
| #661 | List of matching tokens |
| #662 | """ |
| #663 | return await self._get( |
| #664 | "/defi/v3/search", |
| #665 | params={"keyword": query, "limit": limit} |
| #666 | ) |
| #667 | |
| #668 | async def get_trending_tokens( |
| #669 | self, |
| #670 | sort_by: str = "v24hUSD", |
| #671 | sort_type: str = "desc", |
| #672 | offset: int = 0, |
| #673 | limit: int = 20, |
| #674 | ) -> list[dict]: |
| #675 | """ |
| #676 | Get trending/top tokens. |
| #677 | |
| #678 | Args: |
| #679 | sort_by: Sort field ('v24hUSD', 'mc', 'liquidity', 'v24hChangePercent') |
| #680 | sort_type: 'asc' or 'desc' |
| #681 | offset: Pagination offset |
| #682 | limit: Max results |
| #683 | |
| #684 | Returns: |
| #685 | List of trending tokens |
| #686 | """ |
| #687 | # Use v3 token list endpoint |
| #688 | try: |
| #689 | data = await self._get( |
| #690 | "/defi/v3/token/list", |
| #691 | params={ |
| #692 | "sort_by": sort_by, |
| #693 | "sort_type": sort_type, |
| #694 | "offset": offset, |
| #695 | "limit": limit, |
| #696 | } |
| #697 | ) |
| #698 | return data.get("tokens", []) if isinstance(data, dict) else data |
| #699 | except Exception: |
| #700 | # Fallback to search for popular tokens |
| #701 | return await self.search_token("SOL", limit=limit) |
| #702 | |
| #703 | async def get_new_listings(self, limit: int = 50) -> list[dict]: |
| #704 | """ |
| #705 | Get newly listed tokens. |
| #706 | |
| #707 | Args: |
| #708 | limit: Max results |
| #709 | |
| #710 | Returns: |
| #711 | List of new tokens |
| #712 | """ |
| #713 | return await self._get( |
| #714 | "/defi/v2/tokens/new_listing", |
| #715 | params={"limit": limit} |
| #716 | ) |
| #717 | |
| #718 | async def get_gainers_losers( |
| #719 | self, |
| #720 | time_type: str = "24h", |
| #721 | sort_type: str = "desc", |
| #722 | limit: int = 20, |
| #723 | ) -> list[dict]: |
| #724 | """ |
| #725 | Get top gainers or losers. |
| #726 | |
| #727 | Args: |
| #728 | time_type: '24h', '7d', etc. |
| #729 | sort_type: 'desc' for gainers, 'asc' for losers |
| #730 | limit: Max results |
| #731 | |
| #732 | Returns: |
| #733 | List of tokens |
| #734 | """ |
| #735 | return await self._get( |
| #736 | "/defi/price_change", |
| #737 | params={ |
| #738 | "type": time_type, |
| #739 | "sort_type": sort_type, |
| #740 | "limit": limit, |
| #741 | } |
| #742 | ) |
| #743 | |
| #744 | # ===================== |
| #745 | # Pair/Pool Methods |
| #746 | # ===================== |
| #747 | |
| #748 | async def get_token_markets(self, mint: str, limit: int = 20) -> list[dict]: |
| #749 | """ |
| #750 | Get all markets/pairs for a token. |
| #751 | |
| #752 | Args: |
| #753 | mint: Token mint address |
| #754 | limit: Max results |
| #755 | |
| #756 | Returns: |
| #757 | List of pairs |
| #758 | """ |
| #759 | return await self._get( |
| #760 | "/defi/v2/markets", |
| #761 | params={"address": mint, "limit": limit} |
| #762 | ) |
| #763 | |
| #764 | async def get_pair_overview(self, pair_address: str) -> dict: |
| #765 | """ |
| #766 | Get pair/pool overview. |
| #767 | |
| #768 | Args: |
| #769 | pair_address: Pair address |
| #770 | |
| #771 | Returns: |
| #772 | Pair data |
| #773 | """ |
| #774 | return await self._get("/defi/pair_overview", params={"address": pair_address}) |
| #775 | |
| #776 | # ===================== |
| #777 | # Utility Methods |
| #778 | # ===================== |
| #779 | |
| #780 | async def get_supported_chains(self) -> list[str]: |
| #781 | """Get list of supported blockchains""" |
| #782 | data = await self._get("/v1/public/chain/list") |
| #783 | return [chain.get("name") for chain in data] if isinstance(data, list) else [] |
| #784 | |
| #785 | async def close(self): |
| #786 | """Close the HTTP client""" |
| #787 | await self._client.aclose() |
| #788 | |
| #789 | async def __aenter__(self): |
| #790 | return self |
| #791 | |
| #792 | async def __aexit__(self, exc_type, exc_val, exc_tb): |
| #793 | await self.close() |
| #794 | |
| #795 | |
| #796 | class BirdeyeWebSocketClient: |
| #797 | """WebSocket client for real-time Birdeye data streams""" |
| #798 | |
| #799 | def __init__(self, api_key: str, chain: str = "solana"): |
| #800 | """ |
| #801 | Initialize Birdeye WebSocket client. |
| #802 | |
| #803 | Args: |
| #804 | api_key: Birdeye API key |
| #805 | chain: Blockchain (default: solana) |
| #806 | """ |
| #807 | self.api_key = api_key |
| #808 | self.chain = chain |
| #809 | self.ws_url = f"{BIRDEYE_WSS_URL}?x-api-key={api_key}" |
| #810 | |
| #811 | self.websocket: Optional[websockets.WebSocketClientProtocol] = None |
| #812 | self.handlers: Dict[str, List[Callable]] = {} |
| #813 | self.is_connected = False |
| #814 | self.reconnect_attempts = 0 |
| #815 | self.max_reconnect_attempts = 10 |
| #816 | self.ping_task: Optional[asyncio.Task] = None |
| #817 | self.listen_task: Optional[asyncio.Task] = None |
| #818 | |
| #819 | async def connect(self): |
| #820 | """Establish WebSocket connection""" |
| #821 | try: |
| #822 | self.websocket = await websockets.connect(self.ws_url) |
| #823 | self.is_connected = True |
| #824 | self.reconnect_attempts = 0 |
| #825 | print(f"✓ Connected to Birdeye WebSocket ({self.chain})") |
| #826 | |
| #827 | # Start ping-pong task |
| #828 | self.ping_task = asyncio.create_task(self._ping_loop()) |
| #829 | |
| #830 | # Start listening task |
| #831 | self.listen_task = asyncio.create_task(self._listen_loop()) |
| #832 | |
| #833 | except Exception as e: |
| #834 | print(f"Failed to connect to Birdeye WebSocket: {e}") |
| #835 | self.is_connected = False |
| #836 | raise |
| #837 | |
| #838 | async def disconnect(self): |
| #839 | """Close WebSocket connection""" |
| #840 | self.is_connected = False |
| #841 | |
| #842 | if self.ping_task: |
| #843 | self.ping_task.cancel() |
| #844 | if self.listen_task: |
| #845 | self.listen_task.cancel() |
| #846 | |
| #847 | if self.websocket: |
| #848 | await self.websocket.close() |
| #849 | |
| #850 | print("Disconnected from Birdeye WebSocket") |
| #851 | |
| #852 | async def _ping_loop(self): |
| #853 | """Send periodic ping to keep connection alive""" |
| #854 | while self.is_connected: |
| #855 | try: |
| #856 | if self.websocket: |
| #857 | await self.websocket.ping() |
| #858 | await asyncio.sleep(30) # Ping every 30 seconds |
| #859 | except Exception as e: |
| #860 | print(f"Ping failed: {e}") |
| #861 | await self._reconnect() |
| #862 | |
| #863 | async def _listen_loop(self): |
| #864 | """Listen for incoming WebSocket messages""" |
| #865 | while self.is_connected: |
| #866 | try: |
| #867 | if not self.websocket: |
| #868 | break |
| #869 | |
| #870 | message = await self.websocket.recv() |
| #871 | data = json.loads(message) |
| #872 | |
| #873 | # Handle different message types |
| #874 | event_type = data.get("type") |
| #875 | if event_type and event_type in self.handlers: |
| #876 | for handler in self.handlers[event_type]: |
| #877 | try: |
| #878 | await handler(data) |
| #879 | except Exception as e: |
| #880 | print(f"Error in handler for {event_type}: {e}") |
| #881 | |
| #882 | except websockets.exceptions.ConnectionClosed: |
| #883 | print("WebSocket connection closed") |
| #884 | await self._reconnect() |
| #885 | except Exception as e: |
| #886 | print(f"Error in listen loop: {e}") |
| #887 | await asyncio.sleep(1) |
| #888 | |
| #889 | async def _reconnect(self): |
| #890 | """Attempt to reconnect""" |
| #891 | if not self.is_connected or self.reconnect_attempts >= self.max_reconnect_attempts: |
| #892 | return |
| #893 | |
| #894 | self.reconnect_attempts += 1 |
| #895 | print(f"Reconnecting... (attempt {self.reconnect_attempts}/{self.max_reconnect_attempts})") |
| #896 | |
| #897 | await asyncio.sleep(min(2 ** self.reconnect_attempts, 60)) # Exponential backoff |
| #898 | |
| #899 | try: |
| #900 | await self.connect() |
| #901 | except Exception as e: |
| #902 | print(f"Reconnection failed: {e}") |
| #903 | |
| #904 | def on(self, event_type: str, handler: Callable): |
| #905 | """ |
| #906 | Register an event handler. |
| #907 | |
| #908 | Args: |
| #909 | event_type: Event type to listen for |
| #910 | handler: Async callback function |
| #911 | """ |
| #912 | if event_type not in self.handlers: |
| #913 | self.handlers[event_type] = [] |
| #914 | self.handlers[event_type].append(handler) |
| #915 | |
| #916 | async def subscribe_price(self, address: str): |
| #917 | """ |
| #918 | Subscribe to real-time OHLCV price updates. |
| #919 | |
| #920 | Args: |
| #921 | address: Token address |
| #922 | """ |
| #923 | await self._send({ |
| #924 | "type": BirdeyeWSEvent.SUBSCRIBE_PRICE, |
| #925 | "data": {"address": address} |
| #926 | }) |
| #927 | |
| #928 | async def subscribe_base_quote_price(self, base_address: str, quote_address: str): |
| #929 | """ |
| #930 | Subscribe to base-quote pair price updates. |
| #931 | |
| #932 | Args: |
| #933 | base_address: Base token address |
| #934 | quote_address: Quote token address |
| #935 | """ |
| #936 | await self._send({ |
| #937 | "type": BirdeyeWSEvent.SUBSCRIBE_BASE_QUOTE_PRICE, |
| #938 | "data": { |
| #939 | "baseAddress": base_address, |
| #940 | "quoteAddress": quote_address |
| #941 | } |
| #942 | }) |
| #943 | |
| #944 | async def subscribe_new_listings(self): |
| #945 | """Subscribe to new token listings""" |
| #946 | await self._send({ |
| #947 | "type": BirdeyeWSEvent.SUBSCRIBE_TOKEN_NEW_LISTING, |
| #948 | "data": {} |
| #949 | }) |
| #950 | |
| #951 | async def subscribe_new_pairs(self): |
| #952 | """Subscribe to new liquidity pair listings""" |
| #953 | await self._send({ |
| #954 | "type": BirdeyeWSEvent.SUBSCRIBE_NEW_PAIR, |
| #955 | "data": {} |
| #956 | }) |
| #957 | |
| #958 | async def subscribe_large_trades(self, threshold_usd: float = 10000): |
| #959 | """ |
| #960 | Subscribe to large trade transactions. |
| #961 | |
| #962 | Args: |
| #963 | threshold_usd: Minimum trade value in USD |
| #964 | """ |
| #965 | await self._send({ |
| #966 | "type": BirdeyeWSEvent.SUBSCRIBE_LARGE_TRADE_TXS, |
| #967 | "data": {"threshold": threshold_usd} |
| #968 | }) |
| #969 | |
| #970 | async def subscribe_wallet_transactions(self, wallet_address: str): |
| #971 | """ |
| #972 | Subscribe to transactions for a specific wallet. |
| #973 | |
| #974 | Args: |
| #975 | wallet_address: Wallet address to monitor |
| #976 | """ |
| #977 | await self._send({ |
| #978 | "type": BirdeyeWSEvent.SUBSCRIBE_WALLET_TXS, |
| #979 | "data": {"address": wallet_address} |
| #980 | }) |
| #981 | |
| #982 | async def subscribe_token_stats(self, address: str): |
| #983 | """ |
| #984 | Subscribe to token statistics updates. |
| #985 | |
| #986 | Args: |
| #987 | address: Token address |
| #988 | """ |
| #989 | await self._send({ |
| #990 | "type": BirdeyeWSEvent.SUBSCRIBE_TOKEN_STATS, |
| #991 | "data": {"address": address} |
| #992 | }) |
| #993 | |
| #994 | async def _send(self, message: dict): |
| #995 | """Send a message to the WebSocket""" |
| #996 | if not self.websocket or not self.is_connected: |
| #997 | raise Exception("WebSocket not connected") |
| #998 | |
| #999 | await self.websocket.send(json.dumps(message)) |
| #1000 | |
| #1001 | async def __aenter__(self): |
| #1002 | await self.connect() |
| #1003 | return self |
| #1004 | |
| #1005 | async def __aexit__(self, exc_type, exc_val, exc_tb): |
| #1006 | await self.disconnect() |
| #1007 |