From cbcda96ebc9484c91f837860846911c3113c9f03 Mon Sep 17 00:00:00 2001 From: Kiearn Williams Date: Fri, 21 Feb 2025 00:37:04 +0000 Subject: [PATCH 1/3] feat: defillama TVL API integration skills --- skills/defillama/__init__.py | 232 ++++++++++ skills/defillama/api.py | 259 +++++++++++ skills/defillama/base.py | 129 ++++++ skills/defillama/config/chains.py | 429 ++++++++++++++++++ .../defillama/fetch_chain_historical_tvl.py | 130 ++++++ skills/defillama/fetch_chains.py | 142 ++++++ skills/defillama/fetch_historical_tvl.py | 103 +++++ skills/defillama/fetch_protocol.py | 175 +++++++ .../defillama/fetch_protocol_current_tvl.py | 108 +++++ skills/defillama/fetch_protocols.py | 170 +++++++ 10 files changed, 1877 insertions(+) create mode 100644 skills/defillama/__init__.py create mode 100644 skills/defillama/api.py create mode 100644 skills/defillama/base.py create mode 100644 skills/defillama/config/chains.py create mode 100644 skills/defillama/fetch_chain_historical_tvl.py create mode 100644 skills/defillama/fetch_chains.py create mode 100644 skills/defillama/fetch_historical_tvl.py create mode 100644 skills/defillama/fetch_protocol.py create mode 100644 skills/defillama/fetch_protocol_current_tvl.py create mode 100644 skills/defillama/fetch_protocols.py diff --git a/skills/defillama/__init__.py b/skills/defillama/__init__.py new file mode 100644 index 0000000..e056849 --- /dev/null +++ b/skills/defillama/__init__.py @@ -0,0 +1,232 @@ +"""DeFi Llama skills.""" + +from abstracts.agent import AgentStoreABC +from abstracts.skill import SkillStoreABC +from skills.defillama.base import DefiLlamaBaseTool + +# TVL Tools +from skills.defillama.tvl.fetch_protocols import DefiLlamaFetchProtocols +from skills.defillama.tvl.fetch_protocol import DefiLlamaFetchProtocol +from skills.defillama.tvl.fetch_historical_tvl import DefiLlamaFetchHistoricalTvl +from skills.defillama.tvl.fetch_protocol_current_tvl import DefiLlamaFetchProtocolCurrentTvl +from skills.defillama.tvl.fetch_chains import DefiLlamaFetchChains + +# Coins Tools +from skills.defillama.coins.fetch_current_prices import DefiLlamaFetchCurrentPrices +from skills.defillama.coins.fetch_historical_prices import DefiLlamaFetchHistoricalPrices +from skills.defillama.coins.fetch_batch_historical import DefiLlamaFetchBatchHistorical +from skills.defillama.coins.fetch_price_chart import DefiLlamaFetchPriceChart +from skills.defillama.coins.fetch_price_percentage import DefiLlamaFetchPricePercentage +from skills.defillama.coins.fetch_first_price import DefiLlamaFetchFirstPrice +from skills.defillama.coins.fetch_block import DefiLlamaFetchBlock + +# Stablecoins Tools +from skills.defillama.stablecoins.fetch_stablecoins import DefiLlamaFetchStablecoins +from skills.defillama.stablecoins.fetch_stablecoin_charts import DefiLlamaFetchStablecoinCharts +from skills.defillama.stablecoins.fetch_stablecoin_asset import DefiLlamaFetchStablecoinAsset +from skills.defillama.stablecoins.fetch_stablecoin_chains import DefiLlamaFetchStablecoinChains +from skills.defillama.stablecoins.fetch_stablecoin_prices import DefiLlamaFetchStablecoinPrices + +# Yields Tools +from skills.defillama.yields.fetch_pools import DefiLlamaFetchPools +from skills.defillama.yields.fetch_pool_chart import DefiLlamaFetchPoolChart + +# Volumes Tools +from skills.defillama.volumes.fetch_dex_overview import DefiLlamaFetchDexOverview +from skills.defillama.volumes.fetch_dex_summary import DefiLlamaFetchDexSummary +from skills.defillama.volumes.fetch_options_overview import DefiLlamaFetchOptionsOverview +from skills.defillama.volumes.fetch_options_summary import DefiLlamaFetchOptionsSummary + +# Fees Tools +from skills.defillama.fees.fetch_fees_overview import DefiLlamaFetchFeesOverview +from skills.defillama.fees.fetch_fees_summary import DefiLlamaFetchFeesSummary + + +def get_defillama_skill( + name: str, + store: SkillStoreABC, + agent_id: str, + agent_store: AgentStoreABC, +) -> DefiLlamaBaseTool: + """Get a DeFi Llama skill by name. + + Args: + name: The name of the skill to get + store: The skill store for persisting data + agent_id: The ID of the agent + agent_store: The agent store for persisting data + + Returns: + The requested DeFi Llama skill + + Raises: + ValueError: If the requested skill name is unknown + + Notes: + Each skill maps to a specific DeFi Llama API endpoint. Some skills handle both + base and chain-specific endpoints through optional parameters rather than + separate implementations. + """ + # TVL Skills + if name == "fetch_protocols": + return DefiLlamaFetchProtocols( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_protocol": + return DefiLlamaFetchProtocol( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_historical_tvl": # Handles both base and chain-specific endpoints + return DefiLlamaFetchHistoricalTvl( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_protocol_current_tvl": + return DefiLlamaFetchProtocolCurrentTvl( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_chains": + return DefiLlamaFetchChains( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + + # Coins Skills + elif name == "fetch_current_prices": + return DefiLlamaFetchCurrentPrices( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_historical_prices": + return DefiLlamaFetchHistoricalPrices( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_batch_historical": + return DefiLlamaFetchBatchHistorical( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_price_chart": + return DefiLlamaFetchPriceChart( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_price_percentage": + return DefiLlamaFetchPricePercentage( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_first_price": + return DefiLlamaFetchFirstPrice( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_block": + return DefiLlamaFetchBlock( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + + # Stablecoins Skills + elif name == "fetch_stablecoins": + return DefiLlamaFetchStablecoins( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_stablecoin_charts": # Handles both all and chain-specific charts + return DefiLlamaFetchStablecoinCharts( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_stablecoin_asset": + return DefiLlamaFetchStablecoinAsset( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_stablecoin_chains": + return DefiLlamaFetchStablecoinChains( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_stablecoin_prices": + return DefiLlamaFetchStablecoinPrices( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + + # Yields Skills + elif name == "fetch_pools": + return DefiLlamaFetchPools( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_pool_chart": + return DefiLlamaFetchPoolChart( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + + # Volumes Skills + elif name == "fetch_dex_overview": # Handles both base and chain-specific overviews + return DefiLlamaFetchDexOverview( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_dex_summary": + return DefiLlamaFetchDexSummary( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_options_overview": # Handles both base and chain-specific overviews + return DefiLlamaFetchOptionsOverview( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_options_summary": + return DefiLlamaFetchOptionsSummary( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + + # Fees Skills + elif name == "fetch_fees_overview": # Handles both base and chain-specific overviews + return DefiLlamaFetchFeesOverview( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + elif name == "fetch_fees_summary": + return DefiLlamaFetchFeesSummary( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) + else: + raise ValueError(f"Unknown DeFi Llama skill: {name}") diff --git a/skills/defillama/api.py b/skills/defillama/api.py new file mode 100644 index 0000000..729b520 --- /dev/null +++ b/skills/defillama/api.py @@ -0,0 +1,259 @@ +"""DeFi Llama API implementation and shared schemas.""" + +from typing import List, Optional, Union +from datetime import datetime + +import httpx +from pydantic import BaseModel, Field + +DEFILLAMA_BASE_URL = "https://api.llama.fi" + +# TVL API Functions +async def fetch_protocols() -> dict: + """List all protocols on defillama along with their TVL.""" + url = f"{DEFILLAMA_BASE_URL}/protocols" + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_protocol(protocol: str) -> dict: + """Get historical TVL of a protocol and breakdowns by token and chain.""" + url = f"{DEFILLAMA_BASE_URL}/protocol/{protocol}" + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_historical_tvl() -> dict: + """Get historical TVL of DeFi on all chains.""" + url = f"{DEFILLAMA_BASE_URL}/v2/historicalChainTvl" + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_chain_historical_tvl(chain: str) -> dict: + """Get historical TVL of a specific chain.""" + url = f"{DEFILLAMA_BASE_URL}/v2/historicalChainTvl/{chain}" + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_protocol_current_tvl(protocol: str) -> dict: + """Get current TVL of a protocol.""" + url = f"{DEFILLAMA_BASE_URL}/tvl/{protocol}" + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_chains() -> dict: + """Get current TVL of all chains.""" + url = f"{DEFILLAMA_BASE_URL}/v2/chains" + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +# Coins API Functions ----- check if they need additional query params bellow +# async def fetch_current_prices(coins: List[str]) -> dict: +# """Get current prices of tokens by contract address.""" +# coins_str = ','.join(coins) +# url = f"{DEFILLAMA_BASE_URL}/prices/current/{coins_str}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_historical_prices(timestamp: int, coins: List[str]) -> dict: +# """Get historical prices of tokens by contract address.""" +# coins_str = ','.join(coins) +# url = f"{DEFILLAMA_BASE_URL}/prices/historical/{timestamp}/{coins_str}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_batch_historical_prices(coins: List[str], timestamps: List[int]) -> dict: +# """Get historical prices for multiple tokens at multiple timestamps.""" +# url = f"{DEFILLAMA_BASE_URL}/batchHistorical" +# data = {"coins": coins, "timestamps": timestamps} +# async with httpx.AsyncClient() as client: +# response = await client.post(url, json=data) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_price_chart(coins: List[str]) -> dict: +# """Get token prices at regular time intervals.""" +# coins_str = ','.join(coins) +# url = f"{DEFILLAMA_BASE_URL}/chart/{coins_str}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_price_percentage(coins: List[str]) -> dict: +# """Get percentage change in price over time.""" +# coins_str = ','.join(coins) +# url = f"{DEFILLAMA_BASE_URL}/percentage/{coins_str}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_first_price(coins: List[str]) -> dict: +# """Get earliest timestamp price record for coins.""" +# coins_str = ','.join(coins) +# url = f"{DEFILLAMA_BASE_URL}/prices/first/{coins_str}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_block(chain: str, timestamp: int) -> dict: +# """Get the closest block to a timestamp.""" +# url = f"{DEFILLAMA_BASE_URL}/block/{chain}/{timestamp}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# # Stablecoins API Functions +# async def fetch_stablecoins() -> dict: +# """List all stablecoins along with their circulating amounts.""" +# url = f"{DEFILLAMA_BASE_URL}/stablecoins" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_stablecoin_charts(chain: Optional[str] = None) -> dict: +# """Get historical mcap sum of all stablecoins (optionally by chain).""" +# base_url = f"{DEFILLAMA_BASE_URL}/stablecoincharts" +# url = f"{base_url}/all" if chain is None else f"{base_url}/{chain}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_stablecoin_asset(asset: str) -> dict: +# """Get historical mcap and chain distribution of a stablecoin.""" +# url = f"{DEFILLAMA_BASE_URL}/stablecoin/{asset}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_stablecoin_chains() -> dict: +# """Get current mcap sum of all stablecoins on each chain.""" +# url = f"{DEFILLAMA_BASE_URL}/stablecoinchains" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_stablecoin_prices() -> dict: +# """Get historical prices of all stablecoins.""" +# url = f"{DEFILLAMA_BASE_URL}/stablecoinprices" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# # Yields API Functions +# async def fetch_pools() -> dict: +# """Retrieve the latest data for all pools.""" +# url = f"{DEFILLAMA_BASE_URL}/pools" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_pool_chart(pool: str) -> dict: +# """Get historical APY and TVL of a pool.""" +# url = f"{DEFILLAMA_BASE_URL}/chart/{pool}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# # Volumes API Functions +# async def fetch_dex_overview(chain: Optional[str] = None) -> dict: +# """List all dexs with volume summaries, optionally filtered by chain.""" +# base_url = f"{DEFILLAMA_BASE_URL}/overview/dexs" +# url = base_url if chain is None else f"{base_url}/{chain}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_dex_summary(protocol: str) -> dict: +# """Get summary of dex volume with historical data.""" +# url = f"{DEFILLAMA_BASE_URL}/summary/dexs/{protocol}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_options_overview(chain: Optional[str] = None) -> dict: +# """List all options dexs with volume summaries, optionally filtered by chain.""" +# base_url = f"{DEFILLAMA_BASE_URL}/overview/options" +# url = base_url if chain is None else f"{base_url}/{chain}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_options_summary(protocol: str) -> dict: +# """Get summary of options protocol volume with historical data.""" +# url = f"{DEFILLAMA_BASE_URL}/summary/options/{protocol}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# # Fees and Revenue API Functions +# async def fetch_fees_overview(chain: Optional[str] = None) -> dict: +# """List all protocols with fees and revenue summaries, optionally filtered by chain.""" +# base_url = f"{DEFILLAMA_BASE_URL}/overview/fees" +# url = base_url if chain is None else f"{base_url}/{chain}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() +# +# async def fetch_fees_summary(protocol: str) -> dict: +# """Get summary of protocol fees and revenue with historical data.""" +# url = f"{DEFILLAMA_BASE_URL}/summary/fees/{protocol}" +# async with httpx.AsyncClient() as client: +# response = await client.get(url) +# if response.status_code != 200: +# return {"error": f"API returned status code {response.status_code}"} +# return response.json() diff --git a/skills/defillama/base.py b/skills/defillama/base.py new file mode 100644 index 0000000..1682f20 --- /dev/null +++ b/skills/defillama/base.py @@ -0,0 +1,129 @@ +"""Base class for all DeFi Llama tools.""" + +from datetime import datetime, timedelta, timezone +from typing import Type +from pydantic import BaseModel, Field + +from abstracts.agent import AgentStoreABC +from abstracts.skill import IntentKitSkill, SkillStoreABC +from skills.defillama.config.chains import ( + get_chain_from_alias, + is_valid_chain, + get_all_chains, + get_chain_aliases, +) + +DEFILLAMA_BASE_URL = "https://api.llama.fi" + +class DefiLlamaBaseTool(IntentKitSkill): + """Base class for DeFi Llama tools. + + This class provides common functionality for all DeFi Llama API tools: + - Rate limiting + - State management + - Chain validation + - Error handling + """ + + name: str = Field(description="The name of the tool") + description: str = Field(description="A description of what the tool does") + args_schema: Type[BaseModel] + agent_id: str = Field(description="The ID of the agent") + agent_store: AgentStoreABC = Field(description="The agent store for persisting data") + skill_store: SkillStoreABC = Field(description="The skill store for persisting data") + base_url: str = Field(default=DEFILLAMA_BASE_URL, description="Base URL for DeFi Llama API") + + async def check_rate_limit( + self, max_requests: int = 30, interval: int = 1 + ) -> tuple[bool, str | None]: + """Check if the rate limit has been exceeded. + + Args: + max_requests: Maximum requests allowed in the interval (default: 30) + interval: Time interval in minutes (default: 1) + + Returns: + Rate limit status and error message if limited + """ + rate_limit = await self.skill_store.get_agent_skill_data( + self.agent_id, self.name, "rate_limit" + ) + current_time = datetime.now(tz=timezone.utc) + + if ( + rate_limit + and rate_limit.get("reset_time") + and rate_limit.get("count") is not None + and datetime.fromisoformat(rate_limit["reset_time"]) > current_time + ): + if rate_limit["count"] >= max_requests: + return True, "Rate limit exceeded" + + rate_limit["count"] += 1 + await self.skill_store.save_agent_skill_data( + self.agent_id, self.name, "rate_limit", rate_limit + ) + return False, None + + new_rate_limit = { + "count": 1, + "reset_time": (current_time + timedelta(minutes=interval)).isoformat(), + } + await self.skill_store.save_agent_skill_data( + self.agent_id, self.name, "rate_limit", new_rate_limit + ) + return False, None + + async def validate_chain(self, chain: str | None) -> tuple[bool, str | None]: + """Validate and normalize chain parameter. + + Args: + chain: Chain name to validate + + Returns: + Tuple of (is_valid, normalized_chain_name) + """ + if chain is None: + return True, None + + normalized_chain = get_chain_from_alias(chain) + if normalized_chain is None: + return False, None + + return True, normalized_chain + + def get_endpoint_url(self, endpoint: str) -> str: + """Construct full endpoint URL. + + Args: + endpoint: API endpoint path + + Returns: + Complete URL for the endpoint + """ + return f"{self.base_url}/{endpoint.lstrip('/')}" + + def format_error_response(self, status_code: int, message: str) -> dict: + """Format error responses consistently. + + Args: + status_code: HTTP status code + message: Error message + + Returns: + Formatted error response dictionary + """ + return { + "error": True, + "status_code": status_code, + "message": message, + "timestamp": datetime.now(tz=timezone.utc).isoformat() + } + + def get_current_timestamp(self) -> int: + """Get current timestamp in UTC. + + Returns: + Current Unix timestamp + """ + return int(datetime.now(tz=timezone.utc).timestamp()) diff --git a/skills/defillama/config/chains.py b/skills/defillama/config/chains.py new file mode 100644 index 0000000..7043f7b --- /dev/null +++ b/skills/defillama/config/chains.py @@ -0,0 +1,429 @@ +"""Chain configuration for DeFi Llama integration. + +This module contains the valid chains and their aliases for use with the DeFi Llama API. +The VALID_CHAINS dictionary maps primary chain identifiers to their known aliases. +""" + +from typing import Dict, List + +# Chain configuration with aliases +VALID_CHAINS: Dict[str, List[str]] = { + "ethereum": ["eth", "eth1", "eth2"], + "solana": ["sol"], + "bitcoin": ["btc"], + "bsc": ["bnb", "bsc"], + "tron": ["trx"], + "base": ["base"], + "berachain": ["bera"], + "arbitrum": ["arb"], + "sui": ["sui"], + "avalanche": ["avax", "ava"], + "aptos": ["apt"], + "polygon": ["matic", "polygon"], + "hyperliquid": ["hyper"], + "op_mainnet": ["op"], + "sonic": ["sonic"], + "core": ["core"], + "zircuit": ["zircuit"], + "cronos": ["cro"], + "bitlayer": ["bit"], + "cardano": ["ada"], + "bsquared": ["b2"], + "mantle": ["mntl"], + "pulsechain": ["pulse"], + "gnosis": ["gnosis"], + "dydx": ["dydx"], + "taiko": ["tk"], + "bob": ["bob"], + "zksync_era": ["zk", "zkSync"], + "linea": ["linea"], + "blast": ["blast"], + "rootstock": ["rs"], + "thorchain": ["thor"], + "ailayer": ["ai"], + "sei": ["sei"], + "eos": ["eos"], + "ton": ["ton"], + "near": ["near"], + "merlin": ["merlin"], + "kava": ["kava"], + "algorand": ["algo"], + "starknet": ["stark"], + "hedera": ["hbar"], + "mixin": ["mixin"], + "scroll": ["scroll"], + "kaia": ["kaia"], + "stacks": ["stx"], + "ronin": ["ronin"], + "osmosis": ["osmo"], + "verus": ["verus"], + "multiversx": ["x"], + "celo": ["celo"], + "xrpl": ["xrpl"], + "fraxtal": ["frax"], + "stellar": ["xlm"], + "bouncebit": ["bounce"], + "wemix3_0": ["wemix"], + "filecoin": ["fil"], + "hydration": ["hydra"], + "fantom": ["ftm"], + "iota_evm": ["iot"], + "manta": ["manta"], + "eclipse": ["eclp"], + "flow": ["flow"], + "injective": ["inj"], + "tezos": ["xtz"], + "soneium": ["son"], + "neutron": ["neut"], + "icp": ["icp"], + "iotex": ["iotex"], + "metis": ["metis"], + "opbnb": ["opbnb"], + "bifrost_network": ["bifrost"], + "flare": ["flare"], + "xdc": ["xdc"], + "morph": ["morph"], + "waves": ["waves"], + "conflux": ["conflux"], + "corn": ["corn"], + "reya_network": ["reya"], + "mode": ["mode"], + "cronos_zkevm": ["cronoszk"], + "telos": ["telos"], + "rollux": ["rollux"], + "zetachain": ["zeta"], + "chainflip": ["flip"], + "fuel_ignition": ["fuel"], + "aurora": ["aurora"], + "map_protocol": ["map"], + "kujira": ["kujira"], + "astar": ["astar"], + "moonbeam": ["moonbeam"], + "story": ["story"], + "abstract": ["abstract"], + "radix": ["radix"], + "zklink_nova": ["zklink"], + "duckchain": ["duck"], + "swellchain": ["swell"], + "apechain": ["ape"], + "icon": ["icx"], + "immutable_zkevm": ["immutable"], + "eos_evm": ["eosevm"], + "bifrost": ["bifrost"], + "k2": ["k2"], + "aelf": ["aelf"], + "fsc": ["fsc"], + "proton": ["proton"], + "secret": ["secret"], + "unichain": ["unichain"], + "neo": ["neo"], + "mayachain": ["maya"], + "canto": ["canto"], + "chiliz": ["chz"], + "x_layer": ["xlayer"], + "polynomial": ["poly"], + "ontology": ["ont"], + "onus": ["onus"], + "bitcoincash": ["bch"], + "terra2": ["terra2"], + "polygon_zkevm": ["polyzk"], + "ink": ["ink"], + "sophon": ["sophon"], + "venom": ["venom"], + "dexalot": ["dexalot"], + "bahamut": ["bahamut"], + "vite": ["vite"], + "dfs_network": ["dfs"], + "ergo": ["ergo"], + "wanchain": ["wan"], + "mantra": ["mantra"], + "doge": ["doge"], + "lisk": ["lisk"], + "alephium": ["alephium"], + "vision": ["vision"], + "dogechain": ["dogechain"], + "nuls": ["nuls"], + "agoric": ["agoric"], + "defichain": ["defi"], + "dymension": ["dym"], + "thundercore": ["tc"], + "godwokenv1": ["godwoken"], + "bevm": ["bevm"], + "litecoin": ["ltc"], + "ux": ["ux"], + "functionx": ["fx"], + "oraichain": ["oraichain"], + "dfk": ["dfk"], + "carbon": ["carbon"], + "beam": ["beam"], + "gravity": ["gravity"], + "horizen_eon": ["horizen"], + "moonriver": ["movr"], + "real": ["real"], + "oasys": ["oasys"], + "hydra": ["hydra"], + "oktchain": ["okt"], + "shibarium": ["shib"], + "world_chain": ["world"], + "interlay": ["interlay"], + "acala": ["acala"], + "elys": ["elys"], + "boba": ["boba"], + "vana": ["vana"], + "harmony": ["harmony"], + "lachain_network": ["lachain"], + "theta": ["theta"], + "ab": ["ab"], + "defiverse": ["defiverse"], + "kcc": ["kcc"], + "oasis_sapphire": ["oasis"], + "etherlink": ["etherlink"], + "wax": ["wax"], + "archway": ["archway"], + "redbelly": ["redbelly"], + "velas": ["velas"], + "equilibrium": ["equilibrium"], + "unit0": ["unit0"], + "ql1": ["ql1"], + "songbird": ["songbird"], + "zilliqa": ["zil"], + "rangers": ["rangers"], + "odyssey": ["odyssey"], + "terra_classic": ["terra"], + "kadena": ["kadena"], + "zero_network": ["zero"], + "elastos": ["elastos"], + "fluence": ["fluence"], + "idex": ["idex"], + "xpla": ["xpla"], + "milkomeda_c1": ["milkomeda"], + "taraxa": ["taraxa"], + "bitrock": ["bitrock"], + "persistence_one": ["persistence"], + "meter": ["meter"], + "arbitrum_nova": ["arbitrumnova"], + "everscale": ["everscale"], + "ultron": ["ultron"], + "fuse": ["fuse"], + "vechain": ["vet"], + "renec": ["renec"], + "shimmerevm": ["shimmer"], + "obyte": ["obyte"], + "nolus": ["nolus"], + "airdao": ["airdao"], + "elysium": ["elysium"], + "xai": ["xai"], + "starcoin": ["starcoin"], + "oasis_emerald": ["oasisem"], + "haqq": ["haqq"], + "nos": ["nos"], + "neon": ["neon"], + "bittorrent": ["btt"], + "csc": ["csc"], + "satoshivm": ["satv"], + "naka": ["naka"], + "edu_chain": ["edu"], + "kintsugi": ["kintsugi"], + "energi": ["energi"], + "rss3": ["rss3"], + "sx_rollup": ["sx"], + "cosmoshub": ["cosmos"], + "saakuru": ["saakuru"], + "boba_bnb": ["boba_bnb"], + "ethereumclassic": ["etc"], + "skale_europa": ["skale"], + "degen": ["degen"], + "mint": ["mint"], + "juno": ["juno"], + "viction": ["viction"], + "evmos": ["evmos"], + "enuls": ["enuls"], + "lightlink": ["lightlink"], + "sanko": ["sanko"], + "karura": ["karura"], + "kardia": ["kardia"], + "superposition": ["super"], + "crab": ["crab"], + "genesys": ["genesys"], + "matchain": ["matchain"], + "chihuahua": ["chihuahua"], + "massa": ["massa"], + "kroma": ["kroma"], + "tombchain": ["tomb"], + "smartbch": ["bchsmart"], + "ancient8": ["ancient8"], + "penumbra": ["penumbra"], + "ethpow": ["ethpow"], + "omax": ["omax"], + "migaloo": ["migaloo"], + "bostrom": ["bostrom"], + "energyweb": ["energyweb"], + "libre": ["libre"], + "defichain_evm": ["defievm"], + "artela": ["artela"], + "dash": ["dash"], + "sora": ["sora"], + "step": ["step"], + "nibiru": ["nibiru"], + "zkfair": ["zkfair"], + "hela": ["hela"], + "godwoken": ["godwoken"], + "shape": ["shape"], + "stargaze": ["stargaze"], + "crossfi": ["crossfi"], + "bitkub_chain": ["bitkub"], + "q_protocol": ["qprotocol"], + "loop": ["loop"], + "parex": ["parex"], + "alv": ["alv"], + "nahmii": ["nahmii"], + "shido": ["shido"], + "electroneum": ["etn"], + "zora": ["zora"], + "astar_zkevm": ["astark"], + "comdex": ["comdex"], + "stratis": ["stratis"], + "polkadex": ["polkadex"], + "meer": ["meer"], + "neo_x_mainnet": ["neo_x"], + "aura_network": ["aura"], + "findora": ["findora"], + "shiden": ["shiden"], + "swan": ["swan"], + "crescent": ["crescent"], + "rari": ["rari"], + "cyber": ["cyber"], + "redstone": ["redstone"], + "silicon_zkevm": ["silicon"], + "endurance": ["endurance"], + "inevm": ["inevm"], + "grove": ["grove"], + "areon_network": ["areon"], + "jbc": ["jbc"], + "planq": ["planq"], + "lachain": ["lachain"], + "rei": ["rei"], + "multivac": ["multivac"], + "cube": ["cube"], + "syscoin": ["syscoin"], + "vinuchain": ["vinuchain"], + "callisto": ["callisto"], + "hpb": ["hpb"], + "ham": ["ham"], + "ethf": ["ethf"], + "gochain": ["gochain"], + "darwinia": ["darwinia"], + "sx_network": ["sx"], + "manta_atlantic": ["atlantic"], + "ontologyevm": ["ontEvm"], + "mvc": ["mvc"], + "sifchain": ["sifchain"], + "plume": ["plume"], + "bitgert": ["bitgert"], + "reichain": ["reichain"], + "bitnet": ["bitnet"], + "tenet": ["tenet"], + "milkomeda_a1": ["milkomedaA1"], + "aeternity": ["aeternity"], + "palm": ["palm"], + "concordium": ["concordium"], + "kopi": ["kopi"], + "asset_chain": ["asset"], + "pego": ["pego"], + "waterfall": ["waterfall"], + "heco": ["heco"], + "exsat": ["exsat"], + "goerli": ["goerli"], + "celestia": ["celestia"], + "bandchain": ["band"], + "sommelier": ["sommelier"], + "stride": ["stride"], + "polkadot": ["dot"], + "kusama": ["kusama"], + "dexit": ["dexit"], + "fusion": ["fusion"], + "boba_avax": ["boba"], + "stafi": ["stafi"], + "empire": ["empire"], + "oxfun": ["oxfun"], + "pryzm": ["pryzm"], + "hoo": ["hoo"], + "echelon": ["echelon"], + "quicksilver": ["quick"], + "clv": ["clv"], + "pokt": ["pokt"], + "dsc": ["dsc"], + "zksync_lite": ["zkLite"], + "nova_network": ["nova"], + "cmp": ["cmp"], + "genshiro": ["genshiro"], + "lamden": ["lamden"], + "polis": ["polis"], + "zyx": ["zyx"], + "ubiq": ["ubiq"], + "heiko": ["heiko"], + "parallel": ["parallel"], + "coti": ["coti"], + "kekchain": ["kek"], + "muuchain": ["muuchain"], + "tlchain": ["tlchain"], + "zeniql": ["zeniql"], + "bitindi": ["bitindi"], + "lung": ["lung"], + "bone": ["bone"], + "lukso": ["lukso"], + "joltify": ["joltify"] +} + +def get_chain_from_alias(alias: str) -> str | None: + """Get the main chain identifier from an alias. + + Args: + alias: The chain alias to look up + + Returns: + The main chain identifier if found, None otherwise + """ + normalized_alias = alias.lower().strip() + + # Check if it's a main chain name + if normalized_alias in VALID_CHAINS: + return normalized_alias + + # Check aliases + for chain, aliases in VALID_CHAINS.items(): + if normalized_alias in [a.lower() for a in aliases]: + return chain + + return None + +def is_valid_chain(chain: str) -> bool: + """Check if a chain identifier is valid. + + Args: + chain: The chain identifier to validate + + Returns: + True if the chain is valid, False otherwise + """ + return get_chain_from_alias(chain) is not None + +def get_all_chains() -> list[str]: + """Get a list of all valid main chain identifiers. + + Returns: + List of all main chain identifiers + """ + return list(VALID_CHAINS.keys()) + +def get_chain_aliases(chain: str) -> list[str]: + """Get all aliases for a given chain. + + Args: + chain: The main chain identifier + + Returns: + List of aliases for the chain, empty list if chain not found + """ + normalized_chain = chain.lower().strip() + return VALID_CHAINS.get(normalized_chain, []) diff --git a/skills/defillama/fetch_chain_historical_tvl.py b/skills/defillama/fetch_chain_historical_tvl.py new file mode 100644 index 0000000..c75fadb --- /dev/null +++ b/skills/defillama/fetch_chain_historical_tvl.py @@ -0,0 +1,130 @@ +"""Tool for fetching chain historical TVL via DeFiLlama API.""" + +from typing import List, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_chain_historical_tvl + +FETCH_HISTORICAL_TVL_PROMPT = """ +This tool fetches historical Total Value Locked (TVL) data for a specific blockchain. +Provide the chain name (e.g., "ethereum", "solana") to get its TVL history. +Returns a time series of TVL values with their corresponding dates. +""" + + +class HistoricalTVLDataPoint(BaseModel): + """Model representing a single TVL data point.""" + + date: int = Field( + ..., + description="Unix timestamp of the TVL measurement" + ) + tvl: float = Field( + ..., + description="Total Value Locked in USD at this timestamp" + ) + + +class FetchChainHistoricalTVLInput(BaseModel): + """Input schema for fetching chain-specific historical TVL data.""" + + chain: str = Field( + ..., + description="Chain name to fetch TVL for (e.g., 'ethereum', 'solana')" + ) + + +class FetchChainHistoricalTVLResponse(BaseModel): + """Response schema for chain-specific historical TVL data.""" + + chain: str = Field( + ..., + description="Normalized chain name" + ) + data: List[HistoricalTVLDataPoint] = Field( + default_factory=list, + description="List of historical TVL data points" + ) + error: str | None = Field( + default=None, + description="Error message if any" + ) + + +class DefiLlamaFetchChainHistoricalTvl(DefiLlamaBaseTool): + """Tool for fetching historical TVL data for a specific blockchain. + + This tool fetches the complete Total Value Locked (TVL) history for a given + blockchain using the DeFiLlama API. It includes rate limiting and chain + validation to ensure reliable data retrieval. + + Example: + tvl_tool = DefiLlamaFetchChainHistoricalTvl( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await tvl_tool._arun(chain="ethereum") + """ + + name: str = "defillama_fetch_chain_historical_tvl" + description: str = FETCH_HISTORICAL_TVL_PROMPT + args_schema: Type[BaseModel] = FetchChainHistoricalTVLInput + + def _run(self, chain: str) -> FetchChainHistoricalTVLResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self, chain: str) -> FetchChainHistoricalTVLResponse: + """Fetch historical TVL data for the given chain. + + Args: + chain: Blockchain name (e.g., "ethereum", "solana") + + Returns: + FetchChainHistoricalTVLResponse containing chain name, TVL history or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchChainHistoricalTVLResponse( + chain=chain, + error=error_msg + ) + + # Validate chain parameter + is_valid, normalized_chain = await self.validate_chain(chain) + if not is_valid or normalized_chain is None: + return FetchChainHistoricalTVLResponse( + chain=chain, + error=f"Invalid chain: {chain}" + ) + + # Fetch TVL history from API + result = await fetch_chain_historical_tvl(normalized_chain) + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchChainHistoricalTVLResponse( + chain=normalized_chain, + error=result["error"] + ) + + # Parse response into our schema + data_points = [ + HistoricalTVLDataPoint(**point) + for point in result + ] + + return FetchChainHistoricalTVLResponse( + chain=normalized_chain, + data=data_points + ) + + except Exception as e: + return FetchChainHistoricalTVLResponse( + chain=chain, + error=str(e) + ) diff --git a/skills/defillama/fetch_chains.py b/skills/defillama/fetch_chains.py new file mode 100644 index 0000000..d4c0694 --- /dev/null +++ b/skills/defillama/fetch_chains.py @@ -0,0 +1,142 @@ +"""Tool for fetching chain TVL data via DeFi Llama API.""" + +from typing import List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_chains + +FETCH_CHAINS_PROMPT = """ +This tool fetches current Total Value Locked (TVL) data for all blockchains tracked by DeFi Llama. +No input parameters are required. Returns a comprehensive list including: +- Chain name and identifiers +- Current TVL in USD +- Chain metadata (token symbol, IDs) +- Aggregated total TVL across all chains +Returns the complete list of chains and total TVL or an error if the request fails. +""" + + +class ChainTVLData(BaseModel): + """Model representing TVL data for a single chain.""" + + name: str = Field( + ..., + description="Chain name" + ) + tvl: float = Field( + ..., + description="Total Value Locked in USD" + ) + gecko_id: Optional[str] = Field( + None, + description="CoinGecko identifier" + ) + token_symbol: Optional[str] = Field( + None, + alias="tokenSymbol", + description="Native token symbol" + ) + cmc_id: Optional[str] = Field( + None, + alias="cmcId", + description="CoinMarketCap identifier" + ) + chain_id: Optional[int | str] = Field( + None, + alias="chainId", + description="Chain identifier" + ) + + +class FetchChainsInput(BaseModel): + """Input schema for fetching all chains' TVL data. + + This endpoint doesn't require any parameters as it returns + TVL data for all chains. + """ + pass + + +class FetchChainsResponse(BaseModel): + """Response schema for all chains' TVL data.""" + + chains: List[ChainTVLData] = Field( + default_factory=list, + description="List of chains with their TVL data" + ) + total_tvl: float = Field( + ..., + description="Total TVL across all chains in USD" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchChains(DefiLlamaBaseTool): + """Tool for fetching current TVL data for all blockchains. + + This tool retrieves the current Total Value Locked (TVL) for all chains + tracked by DeFi Llama, including chain identifiers and metadata. + + Example: + chains_tool = DefiLlamaFetchChains( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await chains_tool._arun() + """ + + name: str = "defillama_fetch_chains" + description: str = FETCH_CHAINS_PROMPT + args_schema: Type[BaseModel] = FetchChainsInput + + def _run(self) -> FetchChainsResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self) -> FetchChainsResponse: + """Fetch TVL data for all chains. + + Returns: + FetchChainsResponse containing chain TVL data and total TVL or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchChainsResponse( + chains=[], + total_tvl=0, + error=error_msg + ) + + # Fetch chains data from API + result = await fetch_chains() + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchChainsResponse( + chains=[], + total_tvl=0, + error=result["error"] + ) + + # Parse chains data and calculate total TVL + chains = [ChainTVLData(**chain_data) for chain_data in result] + total_tvl = sum(chain.tvl for chain in chains) + + return FetchChainsResponse( + chains=chains, + total_tvl=total_tvl + ) + + except Exception as e: + return FetchChainsResponse( + chains=[], + total_tvl=0, + error=str(e) + ) diff --git a/skills/defillama/fetch_historical_tvl.py b/skills/defillama/fetch_historical_tvl.py new file mode 100644 index 0000000..1279546 --- /dev/null +++ b/skills/defillama/fetch_historical_tvl.py @@ -0,0 +1,103 @@ +"""Tool for fetching total historical TVL via DeFiLlama API.""" + +from typing import List, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_historical_tvl + +FETCH_TOTAL_HISTORICAL_TVL_PROMPT = """ +This tool fetches historical Total Value Locked (TVL) data across all blockchains. +Returns a time series of aggregate TVL values with their corresponding dates. +No input parameters are required as this endpoint returns global DeFi TVL data. +""" + + +class HistoricalTVLDataPoint(BaseModel): + """Model representing a single TVL data point.""" + + date: int = Field( + ..., + description="Unix timestamp of the TVL measurement" + ) + tvl: float = Field( + ..., + description="Total Value Locked in USD at this timestamp" + ) + + +class FetchHistoricalTVLInput(BaseModel): + """Input schema for fetching historical TVL data. + + This endpoint doesn't require any parameters as it returns + global TVL data across all chains. + """ + pass + + +class FetchHistoricalTVLResponse(BaseModel): + """Response schema for historical TVL data.""" + + data: List[HistoricalTVLDataPoint] = Field( + default_factory=list, + description="List of historical TVL data points across all chains" + ) + error: str | None = Field( + default=None, + description="Error message if any" + ) + + +class DefiLlamaFetchHistoricalTvl(DefiLlamaBaseTool): + """Tool for fetching historical TVL data across all blockchains. + + This tool fetches the complete Total Value Locked (TVL) history aggregated + across all chains using the DeFiLlama API. It includes rate limiting to + ensure reliable data retrieval. + + Example: + tvl_tool = DefiLlamaFetchHistoricalTvl( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await tvl_tool._arun() + """ + + name: str = "defillama_fetch_total_historical_tvl" + description: str = FETCH_TOTAL_HISTORICAL_TVL_PROMPT + args_schema: Type[BaseModel] = FetchHistoricalTVLInput + + def _run(self) -> FetchHistoricalTVLResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self) -> FetchHistoricalTVLResponse: + """Fetch historical TVL data across all chains. + + Returns: + FetchHistoricalTVLResponse containing TVL history or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchHistoricalTVLResponse(error=error_msg) + + # Fetch TVL history from API + result = await fetch_historical_tvl() + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchHistoricalTVLResponse(error=result["error"]) + + # Parse response into our schema + data_points = [ + HistoricalTVLDataPoint(**point) + for point in result + ] + + return FetchHistoricalTVLResponse(data=data_points) + + except Exception as e: + return FetchHistoricalTVLResponse(error=str(e)) diff --git a/skills/defillama/fetch_protocol.py b/skills/defillama/fetch_protocol.py new file mode 100644 index 0000000..76e534d --- /dev/null +++ b/skills/defillama/fetch_protocol.py @@ -0,0 +1,175 @@ +"""Tool for fetching specific protocol details via DeFi Llama API.""" + +from typing import Dict, List, Optional, Union +from datetime import datetime +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_protocol + +FETCH_PROTOCOL_PROMPT = """ +This tool fetches comprehensive details about a specific DeFi protocol. +Provide the protocol identifier (e.g., "aave", "curve") to get detailed information including: +- Basic protocol information (name, description, website) +- TVL data across different chains +- Token information and historical amounts +- Social media and development links +- Funding history and significant events +- Market metrics and related protocols +Returns complete protocol details or an error if the protocol is not found. +""" + +class TokenAmount(BaseModel): + """Model representing token amounts at a specific date.""" + date: int = Field(..., description="Unix timestamp") + tokens: Dict[str, float] = Field(..., description="Token amounts keyed by symbol") + +class ChainTVLData(BaseModel): + """Model representing TVL data for a specific chain.""" + tvl: List[Dict[str, float]] = Field(..., description="Historical TVL data points") + tokens: Optional[Dict[str, float]] = Field(None, description="Current token amounts") + tokensInUsd: Optional[Dict[str, float]] = Field(None, description="Current token amounts in USD") + +class HistoricalTVL(BaseModel): + """Model representing a historical TVL data point.""" + date: int = Field(..., description="Unix timestamp") + totalLiquidityUSD: float = Field(..., description="Total TVL in USD") + +class Raise(BaseModel): + """Model representing a funding round.""" + date: int = Field(..., description="Funding date") + name: str = Field(..., description="Protocol name") + round: str = Field(..., description="Funding round type") + amount: float = Field(..., description="Amount raised in millions") + chains: List[str] = Field(..., description="Chains involved") + sector: str = Field(..., description="Business sector") + category: str = Field(..., description="Protocol category") + categoryGroup: str = Field(..., description="Category group") + source: str = Field(..., description="Information source") + leadInvestors: List[str] = Field(default_factory=list, description="Lead investors") + otherInvestors: List[str] = Field(default_factory=list, description="Other investors") + valuation: Optional[float] = Field(None, description="Valuation at time of raise") + defillamaId: Optional[str] = Field(None, description="DefiLlama ID") + +class Hallmark(BaseModel): + """Model representing a significant protocol event.""" + timestamp: int + description: str + +class ProtocolDetail(BaseModel): + """Model representing detailed protocol information.""" + # Basic Info + id: str = Field(..., description="Protocol unique identifier") + name: str = Field(..., description="Protocol name") + address: Optional[str] = Field(None, description="Protocol address") + symbol: str = Field(..., description="Protocol token symbol") + url: str = Field(..., description="Protocol website") + description: str = Field(..., description="Protocol description") + logo: str = Field(..., description="Logo URL") + + # Chain Info + chains: List[str] = Field(default_factory=list, description="Supported chains") + currentChainTvls: Dict[str, float] = Field(..., description="Current TVL by chain") + chainTvls: Dict[str, ChainTVLData] = Field(..., description="Historical TVL data by chain") + + # Identifiers + gecko_id: Optional[str] = Field(None, description="CoinGecko ID") + cmcId: Optional[str] = Field(None, description="CoinMarketCap ID") + + # Social & Development + twitter: Optional[str] = Field(None, description="Twitter handle") + treasury: Optional[str] = Field(None, description="Treasury information") + governanceID: Optional[List[str]] = Field(None, description="Governance identifiers") + github: Optional[List[str]] = Field(None, description="GitHub repositories") + + # Protocol Relationships + isParentProtocol: Optional[bool] = Field(None, description="Whether this is a parent protocol") + otherProtocols: Optional[List[str]] = Field(None, description="Related protocols") + + # Historical Data + tokens: List[TokenAmount] = Field(default_factory=list, description="Historical token amounts") + tvl: List[HistoricalTVL] = Field(..., description="Historical TVL data points") + raises: Optional[List[Raise]] = Field(None, description="Funding rounds") + hallmarks: Optional[List[Hallmark]] = Field(None, description="Significant events") + + # Market Data + mcap: Optional[float] = Field(None, description="Market capitalization") + metrics: Dict = Field(default_factory=dict, description="Additional metrics") + +class DefiLlamaProtocolInput(BaseModel): + """Input model for fetching protocol details.""" + protocol: str = Field(..., description="Protocol identifier to fetch") + +class DefiLlamaProtocolOutput(BaseModel): + """Output model for the protocol fetching tool.""" + protocol: Optional[ProtocolDetail] = Field(None, description="Protocol details") + error: Optional[str] = Field(None, description="Error message if any") + +class DefiLlamaFetchProtocol(DefiLlamaBaseTool): + """Tool for fetching detailed protocol information from DeFi Llama. + + This tool retrieves comprehensive information about a specific protocol, + including TVL history, token breakdowns, and metadata. + + Example: + protocol_tool = DefiLlamaFetchProtocol( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await protocol_tool._arun(protocol="aave") + """ + + name: str = "defillama_fetch_protocol" + description: str = FETCH_PROTOCOL_PROMPT + args_schema: Type[BaseModel] = DefiLlamaProtocolInput + + def _run(self, protocol: str) -> DefiLlamaProtocolOutput: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self, protocol: str) -> DefiLlamaProtocolOutput: + """Fetch detailed information about a specific protocol. + + Args: + protocol: Protocol identifier to fetch + + Returns: + DefiLlamaProtocolOutput containing protocol details or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return DefiLlamaProtocolOutput(error=error_msg) + + # Fetch protocol data from API + result = await fetch_protocol(protocol) + + if isinstance(result, dict) and "error" in result: + return DefiLlamaProtocolOutput(error=result["error"]) + + # Process hallmarks if present + hallmarks = None + if "hallmarks" in result: + hallmarks = [ + Hallmark(timestamp=h[0], description=h[1]) + for h in result.get("hallmarks", []) + ] + + # Create raises objects if present + raises = None + if "raises" in result: + raises = [Raise(**r) for r in result.get("raises", [])] + + # Create protocol detail object + protocol_detail = ProtocolDetail( + **{k: v for k, v in result.items() if k not in ["hallmarks", "raises"]}, + hallmarks=hallmarks, + raises=raises + ) + + return DefiLlamaProtocolOutput(protocol=protocol_detail) + + except Exception as e: + return DefiLlamaProtocolOutput(error=str(e)) diff --git a/skills/defillama/fetch_protocol_current_tvl.py b/skills/defillama/fetch_protocol_current_tvl.py new file mode 100644 index 0000000..15683e1 --- /dev/null +++ b/skills/defillama/fetch_protocol_current_tvl.py @@ -0,0 +1,108 @@ +"""Tool for fetching protocol TVL via DeFiLlama API.""" + +from typing import Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_protocol_current_tvl + +FETCH_TVL_PROMPT = """ +This tool fetches the current Total Value Locked (TVL) for a specific DeFi protocol. +Provide the protocol slug (e.g., "aave", "curve") to get its current TVL in USD. +Returns the normalized protocol name and its TVL value. +""" + + +class FetchProtocolCurrentTVLInput(BaseModel): + """Input schema for fetching current protocol TVL.""" + + protocol: str = Field( + ..., + description="Protocol slug to fetch TVL for (e.g., 'aave', 'curve')" + ) + + +class FetchProtocolCurrentTVLResponse(BaseModel): + """Response schema for current protocol TVL.""" + + protocol: str = Field( + ..., + description="Normalized protocol slug" + ) + tvl: float = Field( + ..., + description="Current Total Value Locked in USD" + ) + error: str | None = Field( + default=None, + description="Error message if any" + ) + + +class DefiLlamaFetchProtocolCurrentTvl(DefiLlamaBaseTool): + """Tool for fetching current TVL of a specific DeFi protocol. + + This tool fetches the current Total Value Locked (TVL) for a given protocol + using the DeFiLlama API. It includes rate limiting to avoid API abuse. + + Example: + tvl_tool = DefiLlamaFetchProtocolCurrentTvl( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await tvl_tool._arun(protocol="aave") + """ + + name: str = "defillama_fetch_protocol_tvl" + description: str = FETCH_TVL_PROMPT + args_schema: Type[BaseModel] = FetchProtocolCurrentTVLInput + + def _run(self, protocol: str) -> FetchProtocolCurrentTVLResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self, protocol: str) -> FetchProtocolCurrentTVLResponse: + """Fetch current TVL for the given protocol. + + Args: + protocol: DeFi protocol slug (e.g., "aave", "curve") + + Returns: + FetchProtocolCurrentTVLResponse containing protocol name, TVL value or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchProtocolCurrentTVLResponse( + protocol=protocol, + tvl=0, + error=error_msg + ) + + # Normalize protocol slug + normalized_protocol = protocol.lower().replace(" ", "-") + + # Fetch TVL from API + result = await fetch_protocol_current_tvl(normalized_protocol) + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchProtocolCurrentTVLResponse( + protocol=normalized_protocol, + tvl=0, + error=result["error"] + ) + + return FetchProtocolCurrentTVLResponse( + protocol=normalized_protocol, + tvl=float(result) + ) + + except Exception as e: + return FetchProtocolCurrentTVLResponse( + protocol=protocol, + tvl=0, + error=str(e) + ) diff --git a/skills/defillama/fetch_protocols.py b/skills/defillama/fetch_protocols.py new file mode 100644 index 0000000..f885173 --- /dev/null +++ b/skills/defillama/fetch_protocols.py @@ -0,0 +1,170 @@ +"""Tool for fetching all protocols via DeFi Llama API.""" + +from typing import Dict, List, Optional, Union +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_protocols + +FETCH_PROTOCOLS_PROMPT = """ +This tool fetches information about all protocols tracked by DeFi Llama. +No input parameters are required. Returns comprehensive data for each protocol including: +- Basic information (name, description, website, logo) +- TVL metrics (total and per-chain breakdowns) +- Audit status and security information +- Token details and market metrics +- Chain support and deployment information +- Social media and development links +- Protocol relationships (forks, oracles) +- Historical events and significant updates +Returns the complete list of protocols or an error if the request fails. +""" + +class Hallmark(BaseModel): + """Model representing a protocol hallmark (significant event).""" + timestamp: int + description: str + +class Protocol(BaseModel): + """Model representing a DeFi protocol.""" + # Basic Info + id: str = Field(..., description="Protocol unique identifier") + name: str = Field(..., description="Protocol name") + address: Optional[str] = Field(None, description="Protocol's main contract address") + symbol: str = Field(..., description="Protocol token symbol") + url: Optional[str] = Field(None, description="Protocol website") + description: Optional[str] = Field(None, description="Protocol description") + chain: Optional[str] = Field(None, description="Main chain of the protocol") + logo: Optional[str] = Field(None, description="URL to protocol logo") + + # Audit Information + audits: Union[str, int] = Field("0", description="Number of audits") + audit_note: Optional[str] = Field(None, description="Additional audit information") + audit_links: Optional[List[str]] = Field(None, description="Links to audit reports") + + # External IDs + gecko_id: Optional[str] = Field(None, description="CoinGecko ID") + cmcId: Optional[Union[str, int]] = Field(None, description="CoinMarketCap ID") + + # Classification + category: str = Field(..., description="Protocol category") + chains: List[str] = Field(default_factory=list, description="Chains the protocol operates on") + + # Module and Related Info + module: str = Field(..., description="Module name in DefiLlama") + parentProtocol: Optional[str] = Field(None, description="Parent protocol identifier") + + # Social and Development + twitter: Optional[str] = Field(None, description="Twitter handle") + github: Optional[List[str]] = Field(None, description="GitHub organization names") + + # Protocol Relationships + oracles: List[str] = Field(default_factory=list, description="Oracle services used") + forkedFrom: List[str] = Field(default_factory=list, description="Protocols this one was forked from") + + # Additional Metadata + methodology: Optional[str] = Field(None, description="TVL calculation methodology") + listedAt: Optional[int] = Field(None, description="Timestamp when protocol was listed") + openSource: Optional[bool] = Field(None, description="Whether protocol is open source") + treasury: Optional[str] = Field(None, description="Treasury information") + misrepresentedTokens: Optional[bool] = Field(None, description="Whether tokens are misrepresented") + hallmarks: Optional[List[Hallmark]] = Field(None, description="Significant protocol events") + + # TVL Related Data + tvl: Optional[float] = Field(None, description="Total Value Locked in USD") + chainTvls: Dict[str, float] = Field( + default_factory=dict, + description="TVL breakdown by chain including special types (staking, borrowed, etc.)" + ) + change_1h: Optional[float] = Field(None, description="1 hour TVL change percentage") + change_1d: Optional[float] = Field(None, description="1 day TVL change percentage") + change_7d: Optional[float] = Field(None, description="7 day TVL change percentage") + + # Additional TVL Components + staking: Optional[float] = Field(None, description="Value in staking") + pool2: Optional[float] = Field(None, description="Value in pool2") + borrowed: Optional[float] = Field(None, description="Value borrowed") + + # Token Information + tokenBreakdowns: Dict[str, float] = Field( + default_factory=dict, + description="TVL breakdown by token" + ) + mcap: Optional[float] = Field(None, description="Market capitalization") + +class DefiLlamaProtocolsOutput(BaseModel): + """Output model for the protocols fetching tool.""" + protocols: List[Protocol] = Field( + default_factory=list, + description="List of fetched protocols" + ) + error: Optional[str] = Field(None, description="Error message if any") + +class DefiLlamaFetchProtocols(DefiLlamaBaseTool): + """Tool for fetching all protocols from DeFi Llama. + + This tool retrieves information about all protocols tracked by DeFi Llama, + including their TVL, supported chains, and related metrics. + + Example: + protocols_tool = DefiLlamaFetchProtocols( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await protocols_tool._arun() + """ + + name: str = "defillama_fetch_protocols" + description: str = FETCH_PROTOCOLS_PROMPT + args_schema: Type[BaseModel] = BaseModel # No input parameters needed + + def _run(self) -> DefiLlamaProtocolsOutput: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self) -> DefiLlamaProtocolsOutput: + """Fetch information about all protocols. + + Returns: + DefiLlamaProtocolsOutput containing list of protocols or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return DefiLlamaProtocolsOutput(error=error_msg) + + # Fetch protocols from API + result = await fetch_protocols() + + if isinstance(result, dict) and "error" in result: + return DefiLlamaProtocolsOutput(error=result["error"]) + + # Convert raw data to Protocol models + protocols = [] + for protocol_data in result: + try: + # Process hallmarks if present + hallmarks = None + if "hallmarks" in protocol_data and protocol_data["hallmarks"]: + hallmarks = [ + Hallmark(timestamp=h[0], description=h[1]) + for h in protocol_data["hallmarks"] + ] + + # Create protocol model + protocol = Protocol( + **{k: v for k, v in protocol_data.items() if k != "hallmarks"}, + hallmarks=hallmarks + ) + protocols.append(protocol) + except Exception as e: + # Log error for individual protocol processing but continue with others + print(f"Error processing protocol {protocol_data.get('name', 'unknown')}: {str(e)}") + continue + + return DefiLlamaProtocolsOutput(protocols=protocols) + + except Exception as e: + return DefiLlamaProtocolsOutput(error=str(e)) From 84a7506bd05f32b658c25236c9882db4d66a56eb Mon Sep 17 00:00:00 2001 From: Kiearn Williams Date: Sun, 23 Feb 2025 03:09:20 +0000 Subject: [PATCH 2/3] feat: defi-llama integration --- skills/defillama/__init__.py | 38 +- skills/defillama/api.py | 449 +++++++------ .../coins/fetch_batch_historical_prices.py | 135 ++++ skills/defillama/coins/fetch_block.py | 134 ++++ .../defillama/coins/fetch_current_prices.py | 122 ++++ skills/defillama/coins/fetch_first_price.py | 116 ++++ .../coins/fetch_historical_prices.py | 131 ++++ skills/defillama/coins/fetch_price_chart.py | 134 ++++ .../defillama/coins/fetch_price_percentage.py | 99 +++ skills/defillama/fees/fetch_fees_overview.py | 106 +++ .../stablecoins/fetch_stablecoin_chains.py | 143 ++++ .../stablecoins/fetch_stablecoin_charts.py | 150 +++++ .../stablecoins/fetch_stablecoin_prices.py | 95 +++ .../stablecoins/fetch_stablecoins.py | 170 +++++ .../defillama/tests/api_integration.test.py | 182 +++++ skills/defillama/tests/api_unit.test.py | 619 ++++++++++++++++++ .../{ => tvl}/fetch_chain_historical_tvl.py | 0 skills/defillama/{ => tvl}/fetch_chains.py | 0 .../{ => tvl}/fetch_historical_tvl.py | 0 skills/defillama/{ => tvl}/fetch_protocol.py | 2 +- .../{ => tvl}/fetch_protocol_current_tvl.py | 0 skills/defillama/{ => tvl}/fetch_protocols.py | 2 +- .../defillama/volumes/fetch_dex_overview.py | 212 ++++++ skills/defillama/volumes/fetch_dex_summary.py | 126 ++++ .../volumes/fetch_options_overview.py | 107 +++ skills/defillama/yields/fetch_pool_chart.py | 135 ++++ skills/defillama/yields/fetch_pools.py | 217 ++++++ 27 files changed, 3395 insertions(+), 229 deletions(-) create mode 100644 skills/defillama/coins/fetch_batch_historical_prices.py create mode 100644 skills/defillama/coins/fetch_block.py create mode 100644 skills/defillama/coins/fetch_current_prices.py create mode 100644 skills/defillama/coins/fetch_first_price.py create mode 100644 skills/defillama/coins/fetch_historical_prices.py create mode 100644 skills/defillama/coins/fetch_price_chart.py create mode 100644 skills/defillama/coins/fetch_price_percentage.py create mode 100644 skills/defillama/fees/fetch_fees_overview.py create mode 100644 skills/defillama/stablecoins/fetch_stablecoin_chains.py create mode 100644 skills/defillama/stablecoins/fetch_stablecoin_charts.py create mode 100644 skills/defillama/stablecoins/fetch_stablecoin_prices.py create mode 100644 skills/defillama/stablecoins/fetch_stablecoins.py create mode 100644 skills/defillama/tests/api_integration.test.py create mode 100644 skills/defillama/tests/api_unit.test.py rename skills/defillama/{ => tvl}/fetch_chain_historical_tvl.py (100%) rename skills/defillama/{ => tvl}/fetch_chains.py (100%) rename skills/defillama/{ => tvl}/fetch_historical_tvl.py (100%) rename skills/defillama/{ => tvl}/fetch_protocol.py (99%) rename skills/defillama/{ => tvl}/fetch_protocol_current_tvl.py (100%) rename skills/defillama/{ => tvl}/fetch_protocols.py (99%) create mode 100644 skills/defillama/volumes/fetch_dex_overview.py create mode 100644 skills/defillama/volumes/fetch_dex_summary.py create mode 100644 skills/defillama/volumes/fetch_options_overview.py create mode 100644 skills/defillama/yields/fetch_pool_chart.py create mode 100644 skills/defillama/yields/fetch_pools.py diff --git a/skills/defillama/__init__.py b/skills/defillama/__init__.py index e056849..3f06735 100644 --- a/skills/defillama/__init__.py +++ b/skills/defillama/__init__.py @@ -8,13 +8,14 @@ from skills.defillama.tvl.fetch_protocols import DefiLlamaFetchProtocols from skills.defillama.tvl.fetch_protocol import DefiLlamaFetchProtocol from skills.defillama.tvl.fetch_historical_tvl import DefiLlamaFetchHistoricalTvl +from skills.defillama.tvl.fetch_chain_historical_tvl import DefiLlamaFetchChainHistoricalTvl from skills.defillama.tvl.fetch_protocol_current_tvl import DefiLlamaFetchProtocolCurrentTvl from skills.defillama.tvl.fetch_chains import DefiLlamaFetchChains # Coins Tools from skills.defillama.coins.fetch_current_prices import DefiLlamaFetchCurrentPrices from skills.defillama.coins.fetch_historical_prices import DefiLlamaFetchHistoricalPrices -from skills.defillama.coins.fetch_batch_historical import DefiLlamaFetchBatchHistorical +from skills.defillama.coins.fetch_batch_historical_prices import DefiLlamaFetchBatchHistoricalPrices from skills.defillama.coins.fetch_price_chart import DefiLlamaFetchPriceChart from skills.defillama.coins.fetch_price_percentage import DefiLlamaFetchPricePercentage from skills.defillama.coins.fetch_first_price import DefiLlamaFetchFirstPrice @@ -23,7 +24,6 @@ # Stablecoins Tools from skills.defillama.stablecoins.fetch_stablecoins import DefiLlamaFetchStablecoins from skills.defillama.stablecoins.fetch_stablecoin_charts import DefiLlamaFetchStablecoinCharts -from skills.defillama.stablecoins.fetch_stablecoin_asset import DefiLlamaFetchStablecoinAsset from skills.defillama.stablecoins.fetch_stablecoin_chains import DefiLlamaFetchStablecoinChains from skills.defillama.stablecoins.fetch_stablecoin_prices import DefiLlamaFetchStablecoinPrices @@ -35,12 +35,9 @@ from skills.defillama.volumes.fetch_dex_overview import DefiLlamaFetchDexOverview from skills.defillama.volumes.fetch_dex_summary import DefiLlamaFetchDexSummary from skills.defillama.volumes.fetch_options_overview import DefiLlamaFetchOptionsOverview -from skills.defillama.volumes.fetch_options_summary import DefiLlamaFetchOptionsSummary # Fees Tools from skills.defillama.fees.fetch_fees_overview import DefiLlamaFetchFeesOverview -from skills.defillama.fees.fetch_fees_summary import DefiLlamaFetchFeesSummary - def get_defillama_skill( name: str, @@ -80,12 +77,18 @@ def get_defillama_skill( agent_id=agent_id, agent_store=agent_store, ) - elif name == "fetch_historical_tvl": # Handles both base and chain-specific endpoints + elif name == "fetch_historical_tvl": return DefiLlamaFetchHistoricalTvl( skill_store=store, agent_id=agent_id, agent_store=agent_store, ) + elif name == "fetch_chain_historical_tvl": + return DefiLlamaFetchChainHistoricalTvl( + skill_store=store, + agent_id=agent_id, + agent_store=agent_store, + ) elif name == "fetch_protocol_current_tvl": return DefiLlamaFetchProtocolCurrentTvl( skill_store=store, @@ -112,8 +115,8 @@ def get_defillama_skill( agent_id=agent_id, agent_store=agent_store, ) - elif name == "fetch_batch_historical": - return DefiLlamaFetchBatchHistorical( + elif name == "fetch_batch_historical_prices": + return DefiLlamaFetchBatchHistoricalPrices( skill_store=store, agent_id=agent_id, agent_store=agent_store, @@ -156,12 +159,6 @@ def get_defillama_skill( agent_id=agent_id, agent_store=agent_store, ) - elif name == "fetch_stablecoin_asset": - return DefiLlamaFetchStablecoinAsset( - skill_store=store, - agent_id=agent_id, - agent_store=agent_store, - ) elif name == "fetch_stablecoin_chains": return DefiLlamaFetchStablecoinChains( skill_store=store, @@ -208,12 +205,6 @@ def get_defillama_skill( agent_id=agent_id, agent_store=agent_store, ) - elif name == "fetch_options_summary": - return DefiLlamaFetchOptionsSummary( - skill_store=store, - agent_id=agent_id, - agent_store=agent_store, - ) # Fees Skills elif name == "fetch_fees_overview": # Handles both base and chain-specific overviews @@ -222,11 +213,6 @@ def get_defillama_skill( agent_id=agent_id, agent_store=agent_store, ) - elif name == "fetch_fees_summary": - return DefiLlamaFetchFeesSummary( - skill_store=store, - agent_id=agent_id, - agent_store=agent_store, - ) + else: raise ValueError(f"Unknown DeFi Llama skill: {name}") diff --git a/skills/defillama/api.py b/skills/defillama/api.py index 729b520..891e060 100644 --- a/skills/defillama/api.py +++ b/skills/defillama/api.py @@ -6,12 +6,17 @@ import httpx from pydantic import BaseModel, Field -DEFILLAMA_BASE_URL = "https://api.llama.fi" +DEFILLAMA_TVL_BASE_URL = "https://api.llama.fi" +DEFILLAMA_COINS_BASE_URL = "https://coins.llama.fi" +DEFILLAMA_STABLECOINS_BASE_URL = "https://stablecoins.llama.fi" +DEFILLAMA_YIELDS_BASE_URL = "https://yields.llama.fi" +DEFILLAMA_VOLUMES_BASE_URL = "https://api.llama.fi" +DEFILLAMA_FEES_BASE_URL = "https://api.llama.fi" # TVL API Functions async def fetch_protocols() -> dict: """List all protocols on defillama along with their TVL.""" - url = f"{DEFILLAMA_BASE_URL}/protocols" + url = f"{DEFILLAMA_TVL_BASE_URL}/protocols" async with httpx.AsyncClient() as client: response = await client.get(url) if response.status_code != 200: @@ -20,7 +25,7 @@ async def fetch_protocols() -> dict: async def fetch_protocol(protocol: str) -> dict: """Get historical TVL of a protocol and breakdowns by token and chain.""" - url = f"{DEFILLAMA_BASE_URL}/protocol/{protocol}" + url = f"{DEFILLAMA_TVL_BASE_URL}/protocol/{protocol}" async with httpx.AsyncClient() as client: response = await client.get(url) if response.status_code != 200: @@ -29,7 +34,7 @@ async def fetch_protocol(protocol: str) -> dict: async def fetch_historical_tvl() -> dict: """Get historical TVL of DeFi on all chains.""" - url = f"{DEFILLAMA_BASE_URL}/v2/historicalChainTvl" + url = f"{DEFILLAMA_TVL_BASE_URL}/v2/historicalChainTvl" async with httpx.AsyncClient() as client: response = await client.get(url) if response.status_code != 200: @@ -38,7 +43,7 @@ async def fetch_historical_tvl() -> dict: async def fetch_chain_historical_tvl(chain: str) -> dict: """Get historical TVL of a specific chain.""" - url = f"{DEFILLAMA_BASE_URL}/v2/historicalChainTvl/{chain}" + url = f"{DEFILLAMA_TVL_BASE_URL}/v2/historicalChainTvl/{chain}" async with httpx.AsyncClient() as client: response = await client.get(url) if response.status_code != 200: @@ -47,7 +52,7 @@ async def fetch_chain_historical_tvl(chain: str) -> dict: async def fetch_protocol_current_tvl(protocol: str) -> dict: """Get current TVL of a protocol.""" - url = f"{DEFILLAMA_BASE_URL}/tvl/{protocol}" + url = f"{DEFILLAMA_TVL_BASE_URL}/tvl/{protocol}" async with httpx.AsyncClient() as client: response = await client.get(url) if response.status_code != 200: @@ -56,204 +61,246 @@ async def fetch_protocol_current_tvl(protocol: str) -> dict: async def fetch_chains() -> dict: """Get current TVL of all chains.""" - url = f"{DEFILLAMA_BASE_URL}/v2/chains" + url = f"{DEFILLAMA_TVL_BASE_URL}/v2/chains" async with httpx.AsyncClient() as client: response = await client.get(url) if response.status_code != 200: return {"error": f"API returned status code {response.status_code}"} return response.json() -# Coins API Functions ----- check if they need additional query params bellow -# async def fetch_current_prices(coins: List[str]) -> dict: -# """Get current prices of tokens by contract address.""" -# coins_str = ','.join(coins) -# url = f"{DEFILLAMA_BASE_URL}/prices/current/{coins_str}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_historical_prices(timestamp: int, coins: List[str]) -> dict: -# """Get historical prices of tokens by contract address.""" -# coins_str = ','.join(coins) -# url = f"{DEFILLAMA_BASE_URL}/prices/historical/{timestamp}/{coins_str}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_batch_historical_prices(coins: List[str], timestamps: List[int]) -> dict: -# """Get historical prices for multiple tokens at multiple timestamps.""" -# url = f"{DEFILLAMA_BASE_URL}/batchHistorical" -# data = {"coins": coins, "timestamps": timestamps} -# async with httpx.AsyncClient() as client: -# response = await client.post(url, json=data) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_price_chart(coins: List[str]) -> dict: -# """Get token prices at regular time intervals.""" -# coins_str = ','.join(coins) -# url = f"{DEFILLAMA_BASE_URL}/chart/{coins_str}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_price_percentage(coins: List[str]) -> dict: -# """Get percentage change in price over time.""" -# coins_str = ','.join(coins) -# url = f"{DEFILLAMA_BASE_URL}/percentage/{coins_str}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_first_price(coins: List[str]) -> dict: -# """Get earliest timestamp price record for coins.""" -# coins_str = ','.join(coins) -# url = f"{DEFILLAMA_BASE_URL}/prices/first/{coins_str}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_block(chain: str, timestamp: int) -> dict: -# """Get the closest block to a timestamp.""" -# url = f"{DEFILLAMA_BASE_URL}/block/{chain}/{timestamp}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# # Stablecoins API Functions -# async def fetch_stablecoins() -> dict: -# """List all stablecoins along with their circulating amounts.""" -# url = f"{DEFILLAMA_BASE_URL}/stablecoins" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_stablecoin_charts(chain: Optional[str] = None) -> dict: -# """Get historical mcap sum of all stablecoins (optionally by chain).""" -# base_url = f"{DEFILLAMA_BASE_URL}/stablecoincharts" -# url = f"{base_url}/all" if chain is None else f"{base_url}/{chain}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_stablecoin_asset(asset: str) -> dict: -# """Get historical mcap and chain distribution of a stablecoin.""" -# url = f"{DEFILLAMA_BASE_URL}/stablecoin/{asset}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_stablecoin_chains() -> dict: -# """Get current mcap sum of all stablecoins on each chain.""" -# url = f"{DEFILLAMA_BASE_URL}/stablecoinchains" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_stablecoin_prices() -> dict: -# """Get historical prices of all stablecoins.""" -# url = f"{DEFILLAMA_BASE_URL}/stablecoinprices" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# # Yields API Functions -# async def fetch_pools() -> dict: -# """Retrieve the latest data for all pools.""" -# url = f"{DEFILLAMA_BASE_URL}/pools" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_pool_chart(pool: str) -> dict: -# """Get historical APY and TVL of a pool.""" -# url = f"{DEFILLAMA_BASE_URL}/chart/{pool}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# # Volumes API Functions -# async def fetch_dex_overview(chain: Optional[str] = None) -> dict: -# """List all dexs with volume summaries, optionally filtered by chain.""" -# base_url = f"{DEFILLAMA_BASE_URL}/overview/dexs" -# url = base_url if chain is None else f"{base_url}/{chain}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_dex_summary(protocol: str) -> dict: -# """Get summary of dex volume with historical data.""" -# url = f"{DEFILLAMA_BASE_URL}/summary/dexs/{protocol}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_options_overview(chain: Optional[str] = None) -> dict: -# """List all options dexs with volume summaries, optionally filtered by chain.""" -# base_url = f"{DEFILLAMA_BASE_URL}/overview/options" -# url = base_url if chain is None else f"{base_url}/{chain}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_options_summary(protocol: str) -> dict: -# """Get summary of options protocol volume with historical data.""" -# url = f"{DEFILLAMA_BASE_URL}/summary/options/{protocol}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# # Fees and Revenue API Functions -# async def fetch_fees_overview(chain: Optional[str] = None) -> dict: -# """List all protocols with fees and revenue summaries, optionally filtered by chain.""" -# base_url = f"{DEFILLAMA_BASE_URL}/overview/fees" -# url = base_url if chain is None else f"{base_url}/{chain}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() -# -# async def fetch_fees_summary(protocol: str) -> dict: -# """Get summary of protocol fees and revenue with historical data.""" -# url = f"{DEFILLAMA_BASE_URL}/summary/fees/{protocol}" -# async with httpx.AsyncClient() as client: -# response = await client.get(url) -# if response.status_code != 200: -# return {"error": f"API returned status code {response.status_code}"} -# return response.json() +# Coins API Functions +async def fetch_current_prices(coins: List[str]) -> dict: + """Get current prices of tokens by contract address using a 4-hour search window. """ + coins_str = ','.join(coins) + url = f"{DEFILLAMA_COINS_BASE_URL}/prices/current/{coins_str}?searchWidth=4h" + + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_historical_prices(timestamp: int, coins: List[str]) -> dict: + """Get historical prices of tokens by contract address using a 4-hour search window. """ + coins_str = ','.join(coins) + url = f"{DEFILLAMA_COINS_BASE_URL}/prices/historical/{timestamp}/{coins_str}?searchWidth=4h" + + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_batch_historical_prices(coins_timestamps: dict) -> dict: + """Get historical prices for multiple tokens at multiple timestamps. """ + url = f"{DEFILLAMA_COINS_BASE_URL}/batchHistorical" + + async with httpx.AsyncClient() as client: + response = await client.get( + url, + params={ + "coins": coins_timestamps, + "searchWidth": "600" + } + ) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_price_chart(coins: List[str]) -> dict: + """Get historical price chart data from the past day for multiple tokens.""" + coins_str = ','.join(coins) + start_time = int(datetime.now().timestamp()) - 86400 # now - 1 day + + url = f"{DEFILLAMA_COINS_BASE_URL}/chart/{coins_str}" + params = { + "start": start_time, + "span": 10, + "period": "2d", + "searchWidth": "600" + } + + async with httpx.AsyncClient() as client: + response = await client.get(url, params=params) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_price_percentage(coins: List[str]) -> dict: + """Get price percentage changes for multiple tokens over a 24h period. """ + coins_str = ','.join(coins) + current_timestamp = int(datetime.now().timestamp()) + + url = f"{DEFILLAMA_COINS_BASE_URL}/percentage/{coins_str}" + params = { + "timestamp": current_timestamp, + "lookForward": "false", + "period": "24h" + } + + async with httpx.AsyncClient() as client: + response = await client.get(url, params=params) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_first_price(coins: List[str]) -> dict: + """Get first recorded price data for multiple tokens. """ + coins_str = ','.join(coins) + url = f"{DEFILLAMA_COINS_BASE_URL}/prices/first/{coins_str}" + + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_block(chain: str) -> dict: + """Get current block data for a specific chain. """ + current_timestamp = int(datetime.now().timestamp()) + url = f"{DEFILLAMA_COINS_BASE_URL}/block/{chain}/{current_timestamp}" + + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +# Stablecoins API Functions +async def fetch_stablecoins() -> dict: + """Get comprehensive stablecoin data from DeFi Llama. """ + url = f"{DEFILLAMA_STABLECOINS_BASE_URL}/stablecoins" + params = { + "includePrices": "true" + } + + async with httpx.AsyncClient() as client: + response = await client.get(url, params=params) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_stablecoin_charts(stablecoin_id: str, chain: Optional[str] = None) -> dict: + """Get historical circulating supply data for a stablecoin. """ + base_url = f"{DEFILLAMA_STABLECOINS_BASE_URL}/stablecoincharts" + + # If chain is specified, fetch chain-specific data, otherwise fetch all chains + endpoint = f"/{chain}" if chain else "/all" + url = f"{base_url}{endpoint}?stablecoin={stablecoin_id}" + + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_stablecoin_chains() -> dict: + """Get stablecoin distribution data across all chains. """ + url = f"{DEFILLAMA_STABLECOINS_BASE_URL}/stablecoinchains" + + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_stablecoin_prices() -> dict: + """Get current stablecoin price data. + + Returns: + Dictionary containing stablecoin prices with their dates + """ + url = f"{DEFILLAMA_STABLECOINS_BASE_URL}/stablecoinprices" + + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +# Yields API Functions +async def fetch_pools() -> dict: + """Get comprehensive data for all yield-generating pools. """ + url = f"{DEFILLAMA_YIELDS_BASE_URL}/pools" + + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_pool_chart(pool_id: str) -> dict: + """Get historical chart data for a specific pool. """ + url = f"{DEFILLAMA_YIELDS_BASE_URL}/chart/{pool_id}" + + async with httpx.AsyncClient() as client: + response = await client.get(url) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +# Volumes API Functions +async def fetch_dex_overview() -> dict: + """Get overview data for DEX protocols. """ + url = f"{DEFILLAMA_VOLUMES_BASE_URL}/overview/dexs" + params = { + "excludeTotalDataChart": "true", + "excludeTotalDataChartBreakdown": "true", + "dataType": "dailyVolume" + } + + async with httpx.AsyncClient() as client: + response = await client.get(url, params=params) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_dex_summary(protocol: str) -> dict: + """Get summary data for a specific DEX protocol. """ + url = f"{DEFILLAMA_VOLUMES_BASE_URL}/summary/dexs/{protocol}" + params = { + "excludeTotalDataChart": "true", + "excludeTotalDataChartBreakdown": "true", + "dataType": "dailyVolume" + } + + async with httpx.AsyncClient() as client: + response = await client.get(url, params=params) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +async def fetch_options_overview() -> dict: + """Get overview data for options protocols from DeFi Llama. """ + url = f"{DEFILLAMA_VOLUMES_BASE_URL}/overview/options" + params = { + "excludeTotalDataChart": "true", + "excludeTotalDataChartBreakdown": "true", + "dataType": "dailyPremiumVolume" + } + + async with httpx.AsyncClient() as client: + response = await client.get(url, params=params) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() + +# Fees and Revenue API Functions +async def fetch_fees_overview() -> dict: + """Get overview data for fees from DeFi Llama. + + Returns: + Dictionary containing fees overview data + """ + url = f"{DEFILLAMA_FEES_BASE_URL}/overview/fees" + params = { + "excludeTotalDataChart": "true", + "excludeTotalDataChartBreakdown": "true", + "dataType": "dailyFees" + } + + async with httpx.AsyncClient() as client: + response = await client.get(url, params=params) + if response.status_code != 200: + return {"error": f"API returned status code {response.status_code}"} + return response.json() diff --git a/skills/defillama/coins/fetch_batch_historical_prices.py b/skills/defillama/coins/fetch_batch_historical_prices.py new file mode 100644 index 0000000..eb964bc --- /dev/null +++ b/skills/defillama/coins/fetch_batch_historical_prices.py @@ -0,0 +1,135 @@ +"""Tool for fetching batch historical token prices via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_batch_historical_prices + +FETCH_BATCH_HISTORICAL_PRICES_PROMPT = """ +This tool fetches historical token prices from DeFi Llama for multiple tokens at multiple timestamps. +Provide a dictionary mapping token identifiers to lists of timestamps in the format: +- Ethereum tokens: {"ethereum:0x...": [timestamp1, timestamp2]} +- Other chains: {"chainname:0x...": [timestamp1, timestamp2]} +- CoinGecko IDs: {"coingecko:bitcoin": [timestamp1, timestamp2]} +Returns historical price data including: +- Prices in USD at each timestamp +- Token symbols +- Confidence scores for price data +Uses a 4-hour search window around each specified timestamp. +""" + + +class HistoricalPricePoint(BaseModel): + """Model representing a single historical price point.""" + + timestamp: int = Field( + ..., + description="Unix timestamp of the price data" + ) + price: float = Field( + ..., + description="Token price in USD at the timestamp" + ) + confidence: float = Field( + ..., + description="Confidence score for the price data" + ) + + +class TokenPriceHistory(BaseModel): + """Model representing historical price data for a single token.""" + + symbol: str = Field( + ..., + description="Token symbol" + ) + prices: List[HistoricalPricePoint] = Field( + ..., + description="List of historical price points" + ) + + +class FetchBatchHistoricalPricesInput(BaseModel): + """Input schema for fetching batch historical token prices.""" + + coins_timestamps: Dict[str, List[int]] = Field( + ..., + description="Dictionary mapping token identifiers to lists of timestamps" + ) + + +class FetchBatchHistoricalPricesResponse(BaseModel): + """Response schema for batch historical token prices.""" + + coins: Dict[str, TokenPriceHistory] = Field( + default_factory=dict, + description="Historical token prices keyed by token identifier" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchBatchHistoricalPrices(DefiLlamaBaseTool): + """Tool for fetching batch historical token prices from DeFi Llama. + + This tool retrieves historical prices for multiple tokens at multiple + timestamps, using a 4-hour search window around each requested time. + + Example: + prices_tool = DefiLlamaFetchBatchHistoricalPrices( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await prices_tool._arun( + coins_timestamps={ + "ethereum:0x...": [1640995200, 1641081600], # Jan 1-2, 2022 + "coingecko:bitcoin": [1640995200, 1641081600] + } + ) + """ + + name: str = "defillama_fetch_batch_historical_prices" + description: str = FETCH_BATCH_HISTORICAL_PRICES_PROMPT + args_schema: Type[BaseModel] = FetchBatchHistoricalPricesInput + + def _run( + self, coins_timestamps: Dict[str, List[int]] + ) -> FetchBatchHistoricalPricesResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun( + self, coins_timestamps: Dict[str, List[int]] + ) -> FetchBatchHistoricalPricesResponse: + """Fetch historical prices for the given tokens at specified timestamps. + + Args: + coins_timestamps: Dictionary mapping token identifiers to lists of timestamps + + Returns: + FetchBatchHistoricalPricesResponse containing historical token prices or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchBatchHistoricalPricesResponse(error=error_msg) + + # Fetch batch historical prices from API + result = await fetch_batch_historical_prices( + coins_timestamps=coins_timestamps + ) + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchBatchHistoricalPricesResponse(error=result["error"]) + + # Return the response matching the API structure + return FetchBatchHistoricalPricesResponse(coins=result["coins"]) + + except Exception as e: + return FetchBatchHistoricalPricesResponse(error=str(e)) diff --git a/skills/defillama/coins/fetch_block.py b/skills/defillama/coins/fetch_block.py new file mode 100644 index 0000000..bd01107 --- /dev/null +++ b/skills/defillama/coins/fetch_block.py @@ -0,0 +1,134 @@ +"""Tool for fetching current block data via DeFi Llama API.""" + +from typing import Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_block + +FETCH_BLOCK_PROMPT = """ +This tool fetches current block data from DeFi Llama for a specific chain. +Provide: +- Chain name (e.g. "ethereum", "bsc", "solana") +Returns: +- Block height +- Block timestamp +""" + + +class BlockData(BaseModel): + """Model representing block data.""" + + height: int = Field( + ..., + description="Block height number" + ) + timestamp: int = Field( + ..., + description="Unix timestamp of the block" + ) + + +class FetchBlockInput(BaseModel): + """Input schema for fetching block data.""" + + chain: str = Field( + ..., + description="Chain name to fetch block data for" + ) + + +class FetchBlockResponse(BaseModel): + """Response schema for block data.""" + + chain: str = Field( + ..., + description="Normalized chain name" + ) + height: Optional[int] = Field( + None, + description="Block height number" + ) + timestamp: Optional[int] = Field( + None, + description="Unix timestamp of the block" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchBlock(DefiLlamaBaseTool): + """Tool for fetching current block data from DeFi Llama. + + This tool retrieves current block data for a specific chain. + + Example: + block_tool = DefiLlamaFetchBlock( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await block_tool._arun(chain="ethereum") + """ + + name: str = "defillama_fetch_block" + description: str = FETCH_BLOCK_PROMPT + args_schema: Type[BaseModel] = FetchBlockInput + + def _run( + self, chain: str + ) -> FetchBlockResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun( + self, chain: str + ) -> FetchBlockResponse: + """Fetch current block data for the given chain. + + Args: + chain: Chain name to fetch block data for + + Returns: + FetchBlockResponse containing block data or error + """ + try: + # Validate chain parameter + is_valid, normalized_chain = await self.validate_chain(chain) + if not is_valid or normalized_chain is None: + return FetchBlockResponse( + chain=chain, + error=f"Invalid chain: {chain}" + ) + + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchBlockResponse( + chain=normalized_chain, + error=error_msg + ) + + # Fetch block data from API + result = await fetch_block(chain=normalized_chain) + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchBlockResponse( + chain=normalized_chain, + error=result["error"] + ) + + # Return the response matching the API structure + return FetchBlockResponse( + chain=normalized_chain, + height=result["height"], + timestamp=result["timestamp"] + ) + + except Exception as e: + return FetchBlockResponse( + chain=chain, + error=str(e)) diff --git a/skills/defillama/coins/fetch_current_prices.py b/skills/defillama/coins/fetch_current_prices.py new file mode 100644 index 0000000..a31f305 --- /dev/null +++ b/skills/defillama/coins/fetch_current_prices.py @@ -0,0 +1,122 @@ +"""Tool for fetching token prices via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_current_prices + +FETCH_PRICES_PROMPT = """ +This tool fetches current token prices from DeFi Llama with a 4-hour search window. +Provide a list of token identifiers in the format: +- Ethereum tokens: 'ethereum:0x...' +- Other chains: 'chainname:0x...' +- CoinGecko IDs: 'coingecko:bitcoin' +Returns price data including: +- Current price in USD +- Token symbol +- Price confidence score +- Token decimals (if available) +- Last update timestamp +""" + + +class TokenPrice(BaseModel): + """Model representing token price data.""" + + price: float = Field( + ..., + description="Current token price in USD" + ) + symbol: str = Field( + ..., + description="Token symbol" + ) + timestamp: int = Field( + ..., + description="Unix timestamp of last price update" + ) + confidence: float = Field( + ..., + description="Confidence score for the price data" + ) + decimals: Optional[int] = Field( + None, + description="Token decimals, if available" + ) + + +class FetchCurrentPricesInput(BaseModel): + """Input schema for fetching current token prices with a 4-hour search window.""" + + coins: List[str] = Field( + ..., + description="List of token identifiers (e.g. 'ethereum:0x...', 'coingecko:ethereum')" + ) + + +class FetchCurrentPricesResponse(BaseModel): + """Response schema for current token prices.""" + + coins: Dict[str, TokenPrice] = Field( + default_factory=dict, + description="Token prices keyed by token identifier" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchCurrentPrices(DefiLlamaBaseTool): + """Tool for fetching current token prices from DeFi Llama. + + This tool retrieves current prices for multiple tokens in a single request, + using a 4-hour search window to ensure fresh data. + + Example: + prices_tool = DefiLlamaFetchCurrentPrices( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await prices_tool._arun( + coins=["ethereum:0x...", "coingecko:bitcoin"] + ) + """ + + name: str = "defillama_fetch_current_prices" + description: str = FETCH_PRICES_PROMPT + args_schema: Type[BaseModel] = FetchCurrentPricesInput + + def _run(self, coins: List[str]) -> FetchCurrentPricesResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self, coins: List[str]) -> FetchCurrentPricesResponse: + """Fetch current prices for the given tokens. + + Args: + coins: List of token identifiers to fetch prices for + + Returns: + FetchCurrentPricesResponse containing token prices or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchCurrentPricesResponse(error=error_msg) + + # Fetch prices from API + result = await fetch_current_prices(coins=coins) + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchCurrentPricesResponse(error=result["error"]) + + # Return the response matching the API structure + return FetchCurrentPricesResponse(coins=result["coins"]) + + except Exception as e: + return FetchCurrentPricesResponse(error=str(e)) diff --git a/skills/defillama/coins/fetch_first_price.py b/skills/defillama/coins/fetch_first_price.py new file mode 100644 index 0000000..79d5868 --- /dev/null +++ b/skills/defillama/coins/fetch_first_price.py @@ -0,0 +1,116 @@ +"""Tool for fetching first recorded token prices via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_first_price + +FETCH_FIRST_PRICE_PROMPT = """ +This tool fetches the first recorded price data from DeFi Llama for multiple tokens. +Provide a list of token identifiers in the format: +- Ethereum tokens: 'ethereum:0x...' +- Other chains: 'chainname:0x...' +- CoinGecko IDs: 'coingecko:bitcoin' +Returns first price data including: +- Initial price in USD +- Token symbol +- Timestamp of first recorded price +""" + + +class FirstPriceData(BaseModel): + """Model representing first price data for a single token.""" + + symbol: str = Field( + ..., + description="Token symbol" + ) + price: float = Field( + ..., + description="First recorded price in USD" + ) + timestamp: int = Field( + ..., + description="Unix timestamp of first recorded price" + ) + + +class FetchFirstPriceInput(BaseModel): + """Input schema for fetching first token prices.""" + + coins: List[str] = Field( + ..., + description="List of token identifiers to fetch first prices for" + ) + + +class FetchFirstPriceResponse(BaseModel): + """Response schema for first token prices.""" + + coins: Dict[str, FirstPriceData] = Field( + default_factory=dict, + description="First price data keyed by token identifier" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchFirstPrice(DefiLlamaBaseTool): + """Tool for fetching first recorded token prices from DeFi Llama. + + This tool retrieves the first price data recorded for multiple tokens, + including the initial price, symbol, and timestamp. + + Example: + first_price_tool = DefiLlamaFetchFirstPrice( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await first_price_tool._arun( + coins=["ethereum:0x...", "coingecko:ethereum"] + ) + """ + + name: str = "defillama_fetch_first_price" + description: str = FETCH_FIRST_PRICE_PROMPT + args_schema: Type[BaseModel] = FetchFirstPriceInput + + def _run( + self, coins: List[str] + ) -> FetchFirstPriceResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun( + self, coins: List[str] + ) -> FetchFirstPriceResponse: + """Fetch first recorded prices for the given tokens. + + Args: + coins: List of token identifiers to fetch first prices for + + Returns: + FetchFirstPriceResponse containing first price data or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchFirstPriceResponse(error=error_msg) + + # Fetch first price data from API + result = await fetch_first_price(coins=coins) + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchFirstPriceResponse(error=result["error"]) + + # Return the response matching the API structure + return FetchFirstPriceResponse(coins=result["coins"]) + + except Exception as e: + return FetchFirstPriceResponse(error=str(e)) diff --git a/skills/defillama/coins/fetch_historical_prices.py b/skills/defillama/coins/fetch_historical_prices.py new file mode 100644 index 0000000..4b2d503 --- /dev/null +++ b/skills/defillama/coins/fetch_historical_prices.py @@ -0,0 +1,131 @@ +"""Tool for fetching historical token prices via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_historical_prices + +FETCH_HISTORICAL_PRICES_PROMPT = """ +This tool fetches historical token prices from DeFi Llama for a specific timestamp. +Provide a timestamp and list of token identifiers in the format: +- Ethereum tokens: 'ethereum:0x...' +- Other chains: 'chainname:0x...' +- CoinGecko IDs: 'coingecko:bitcoin' +Returns historical price data including: +- Price in USD at the specified time +- Token symbol +- Token decimals (if available) +- Actual timestamp of the price data +Uses a 4-hour search window around the specified timestamp. +""" + + +class HistoricalTokenPrice(BaseModel): + """Model representing historical token price data.""" + + price: float = Field( + ..., + description="Token price in USD at the specified time" + ) + symbol: Optional[str] = Field( + None, + description="Token symbol" + ) + timestamp: int = Field( + ..., + description="Unix timestamp of the price data" + ) + decimals: Optional[int] = Field( + None, + description="Token decimals, if available" + ) + + +class FetchHistoricalPricesInput(BaseModel): + """Input schema for fetching historical token prices.""" + + timestamp: int = Field( + ..., + description="Unix timestamp for historical price lookup" + ) + coins: List[str] = Field( + ..., + description="List of token identifiers (e.g. 'ethereum:0x...', 'coingecko:ethereum')" + ) + + +class FetchHistoricalPricesResponse(BaseModel): + """Response schema for historical token prices.""" + + coins: Dict[str, HistoricalTokenPrice] = Field( + default_factory=dict, + description="Historical token prices keyed by token identifier" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchHistoricalPrices(DefiLlamaBaseTool): + """Tool for fetching historical token prices from DeFi Llama. + + This tool retrieves historical prices for multiple tokens at a specific + timestamp, using a 4-hour search window around the requested time. + + Example: + prices_tool = DefiLlamaFetchHistoricalPrices( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await prices_tool._arun( + timestamp=1640995200, # Jan 1, 2022 + coins=["ethereum:0x...", "coingecko:bitcoin"] + ) + """ + + name: str = "defillama_fetch_historical_prices" + description: str = FETCH_HISTORICAL_PRICES_PROMPT + args_schema: Type[BaseModel] = FetchHistoricalPricesInput + + def _run( + self, timestamp: int, coins: List[str] + ) -> FetchHistoricalPricesResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun( + self, timestamp: int, coins: List[str] + ) -> FetchHistoricalPricesResponse: + """Fetch historical prices for the given tokens at the specified time. + + Args: + timestamp: Unix timestamp for historical price lookup + coins: List of token identifiers to fetch prices for + + Returns: + FetchHistoricalPricesResponse containing historical token prices or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchHistoricalPricesResponse(error=error_msg) + + # Fetch historical prices from API + result = await fetch_historical_prices( + timestamp=timestamp, + coins=coins + ) + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchHistoricalPricesResponse(error=result["error"]) + + # Return the response matching the API structure + return FetchHistoricalPricesResponse(coins=result["coins"]) + + except Exception as e: + return FetchHistoricalPricesResponse(error=str(e)) diff --git a/skills/defillama/coins/fetch_price_chart.py b/skills/defillama/coins/fetch_price_chart.py new file mode 100644 index 0000000..542210b --- /dev/null +++ b/skills/defillama/coins/fetch_price_chart.py @@ -0,0 +1,134 @@ +"""Tool for fetching token price charts via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_price_chart + +FETCH_PRICE_CHART_PROMPT = """ +This tool fetches price chart data from DeFi Llama for multiple tokens. +Provide a list of token identifiers in the format: +- Ethereum tokens: 'ethereum:0x...' +- Other chains: 'chainname:0x...' +- CoinGecko IDs: 'coingecko:bitcoin' +Returns price chart data including: +- Historical price points for the last 24 hours +- Token symbol and metadata +- Confidence scores for price data +- Token decimals (if available) +""" + + +class PricePoint(BaseModel): + """Model representing a single price point in the chart.""" + + timestamp: int = Field( + ..., + description="Unix timestamp of the price data" + ) + price: float = Field( + ..., + description="Token price in USD at the timestamp" + ) + + +class TokenPriceChart(BaseModel): + """Model representing price chart data for a single token.""" + + symbol: str = Field( + ..., + description="Token symbol" + ) + confidence: float = Field( + ..., + description="Confidence score for the price data" + ) + decimals: Optional[int] = Field( + None, + description="Token decimals" + ) + prices: List[PricePoint] = Field( + ..., + description="List of historical price points" + ) + + +class FetchPriceChartInput(BaseModel): + """Input schema for fetching token price charts.""" + + coins: List[str] = Field( + ..., + description="List of token identifiers to fetch price charts for" + ) + + +class FetchPriceChartResponse(BaseModel): + """Response schema for token price charts.""" + + coins: Dict[str, TokenPriceChart] = Field( + default_factory=dict, + description="Price chart data keyed by token identifier" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchPriceChart(DefiLlamaBaseTool): + """Tool for fetching token price charts from DeFi Llama. + + This tool retrieves price chart data for multiple tokens over the last 24 hours, + including historical price points and token metadata. + + Example: + chart_tool = DefiLlamaFetchPriceChart( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await chart_tool._arun( + coins=["ethereum:0x...", "coingecko:ethereum"] + ) + """ + + name: str = "defillama_fetch_price_chart" + description: str = FETCH_PRICE_CHART_PROMPT + args_schema: Type[BaseModel] = FetchPriceChartInput + + def _run( + self, coins: List[str] + ) -> FetchPriceChartResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun( + self, coins: List[str] + ) -> FetchPriceChartResponse: + """Fetch price charts for the given tokens. + + Args: + coins: List of token identifiers to fetch price charts for + + Returns: + FetchPriceChartResponse containing price chart data or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchPriceChartResponse(error=error_msg) + + # Fetch price chart data from API + result = await fetch_price_chart(coins=coins) + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchPriceChartResponse(error=result["error"]) + + # Return the response matching the API structure + return FetchPriceChartResponse(coins=result["coins"]) + + except Exception as e: + return FetchPriceChartResponse(error=str(e)) diff --git a/skills/defillama/coins/fetch_price_percentage.py b/skills/defillama/coins/fetch_price_percentage.py new file mode 100644 index 0000000..df5667f --- /dev/null +++ b/skills/defillama/coins/fetch_price_percentage.py @@ -0,0 +1,99 @@ +"""Tool for fetching token price percentage changes via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_price_percentage + +FETCH_PRICE_PERCENTAGE_PROMPT = """ +This tool fetches 24-hour price percentage changes from DeFi Llama for multiple tokens. +Provide a list of token identifiers in the format: +- Ethereum tokens: 'ethereum:0x...' +- Other chains: 'chainname:0x...' +- CoinGecko IDs: 'coingecko:bitcoin' +Returns price percentage changes: +- Negative values indicate price decrease +- Positive values indicate price increase +- Changes are calculated from current time +""" + + +class FetchPricePercentageInput(BaseModel): + """Input schema for fetching token price percentage changes.""" + + coins: List[str] = Field( + ..., + description="List of token identifiers to fetch price changes for" + ) + + +class FetchPricePercentageResponse(BaseModel): + """Response schema for token price percentage changes.""" + + coins: Dict[str, float] = Field( + default_factory=dict, + description="Price percentage changes keyed by token identifier" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchPricePercentage(DefiLlamaBaseTool): + """Tool for fetching token price percentage changes from DeFi Llama. + + This tool retrieves 24-hour price percentage changes for multiple tokens, + calculated from the current time. + + Example: + percentage_tool = DefiLlamaFetchPricePercentage( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await percentage_tool._arun( + coins=["ethereum:0x...", "coingecko:ethereum"] + ) + """ + + name: str = "defillama_fetch_price_percentage" + description: str = FETCH_PRICE_PERCENTAGE_PROMPT + args_schema: Type[BaseModel] = FetchPricePercentageInput + + def _run( + self, coins: List[str] + ) -> FetchPricePercentageResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun( + self, coins: List[str] + ) -> FetchPricePercentageResponse: + """Fetch price percentage changes for the given tokens. + + Args: + coins: List of token identifiers to fetch price changes for + + Returns: + FetchPricePercentageResponse containing price percentage changes or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchPricePercentageResponse(error=error_msg) + + # Fetch price percentage data from API + result = await fetch_price_percentage(coins=coins) + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchPricePercentageResponse(error=result["error"]) + + # Return the response matching the API structure + return FetchPricePercentageResponse(coins=result["coins"]) + + except Exception as e: + return FetchPricePercentageResponse(error=str(e)) diff --git a/skills/defillama/fees/fetch_fees_overview.py b/skills/defillama/fees/fetch_fees_overview.py new file mode 100644 index 0000000..3cd7f74 --- /dev/null +++ b/skills/defillama/fees/fetch_fees_overview.py @@ -0,0 +1,106 @@ +"""Tool for fetching fees overview data via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_fees_overview + +FETCH_FEES_OVERVIEW_PROMPT = """ +This tool fetches comprehensive overview data for protocol fees from DeFi Llama. +Returns detailed metrics including: +- Total fees across different timeframes +- Change percentages +- Protocol-specific data +- Chain breakdowns +""" + +class ProtocolMethodology(BaseModel): + """Model representing protocol methodology data.""" + UserFees: Optional[str] = Field(None, description="Description of user fees") + Fees: Optional[str] = Field(None, description="Description of fees") + Revenue: Optional[str] = Field(None, description="Description of revenue") + ProtocolRevenue: Optional[str] = Field(None, description="Description of protocol revenue") + HoldersRevenue: Optional[str] = Field(None, description="Description of holders revenue") + SupplySideRevenue: Optional[str] = Field(None, description="Description of supply side revenue") + +class Protocol(BaseModel): + """Model representing protocol data.""" + name: str = Field(..., description="Protocol name") + displayName: str = Field(..., description="Display name of protocol") + category: str = Field(..., description="Protocol category") + logo: str = Field(..., description="Logo URL") + chains: List[str] = Field(..., description="Supported chains") + module: str = Field(..., description="Protocol module") + total24h: Optional[float] = Field(None, description="24-hour total fees") + total7d: Optional[float] = Field(None, description="7-day total fees") + total30d: Optional[float] = Field(None, description="30-day total fees") + total1y: Optional[float] = Field(None, description="1-year total fees") + totalAllTime: Optional[float] = Field(None, description="All-time total fees") + change_1d: Optional[float] = Field(None, description="24-hour change percentage") + change_7d: Optional[float] = Field(None, description="7-day change percentage") + change_1m: Optional[float] = Field(None, description="30-day change percentage") + methodology: Optional[ProtocolMethodology] = Field(None, description="Protocol methodology") + breakdown24h: Optional[Dict[str, Dict[str, float]]] = Field(None, description="24-hour breakdown by chain") + breakdown30d: Optional[Dict[str, Dict[str, float]]] = Field(None, description="30-day breakdown by chain") + +class FetchFeesOverviewResponse(BaseModel): + """Response schema for fees overview data.""" + total24h: float = Field(..., description="Total fees in last 24 hours") + total7d: float = Field(..., description="Total fees in last 7 days") + total30d: float = Field(..., description="Total fees in last 30 days") + total1y: float = Field(..., description="Total fees in last year") + change_1d: float = Field(..., description="24-hour change percentage") + change_7d: float = Field(..., description="7-day change percentage") + change_1m: float = Field(..., description="30-day change percentage") + allChains: List[str] = Field(..., description="List of all chains") + protocols: List[Protocol] = Field(..., description="List of protocols") + error: Optional[str] = Field(None, description="Error message if any") + +class DefiLlamaFetchFeesOverview(DefiLlamaBaseTool): + """Tool for fetching fees overview data from DeFi Llama. + + This tool retrieves comprehensive data about protocol fees, + including fee metrics, change percentages, and detailed protocol information. + + Example: + overview_tool = DefiLlamaFetchFeesOverview( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await overview_tool._arun() + """ + + name: str = "defillama_fetch_fees_overview" + description: str = FETCH_FEES_OVERVIEW_PROMPT + args_schema: Type[BaseModel] = BaseModel + + def _run(self) -> FetchFeesOverviewResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self) -> FetchFeesOverviewResponse: + """Fetch overview data for protocol fees. + + Returns: + FetchFeesOverviewResponse containing comprehensive fee data or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchFeesOverviewResponse(error=error_msg) + + # Fetch fees data from API + result = await fetch_fees_overview() + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchFeesOverviewResponse(error=result["error"]) + + # Return the parsed response + return FetchFeesOverviewResponse(**result) + + except Exception as e: + return FetchFeesOverviewResponse(error=str(e)) diff --git a/skills/defillama/stablecoins/fetch_stablecoin_chains.py b/skills/defillama/stablecoins/fetch_stablecoin_chains.py new file mode 100644 index 0000000..589df46 --- /dev/null +++ b/skills/defillama/stablecoins/fetch_stablecoin_chains.py @@ -0,0 +1,143 @@ +"""Tool for fetching stablecoin chains data via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_stablecoin_chains + +FETCH_STABLECOIN_CHAINS_PROMPT = """ +This tool fetches stablecoin distribution data across all chains from DeFi Llama. +Returns: +- List of chains with stablecoin circulating amounts +- Token information for each chain +- Peg type circulating amounts (USD, EUR, etc.) +""" + + +class CirculatingUSD(BaseModel): + """Model representing circulating amounts in different pegs.""" + + peggedUSD: Optional[float] = Field( + None, + description="Amount pegged to USD" + ) + peggedEUR: Optional[float] = Field( + None, + description="Amount pegged to EUR" + ) + peggedVAR: Optional[float] = Field( + None, + description="Amount in variable pegs" + ) + peggedJPY: Optional[float] = Field( + None, + description="Amount pegged to JPY" + ) + peggedCHF: Optional[float] = Field( + None, + description="Amount pegged to CHF" + ) + peggedCAD: Optional[float] = Field( + None, + description="Amount pegged to CAD" + ) + peggedGBP: Optional[float] = Field( + None, + description="Amount pegged to GBP" + ) + peggedAUD: Optional[float] = Field( + None, + description="Amount pegged to AUD" + ) + peggedCNY: Optional[float] = Field( + None, + description="Amount pegged to CNY" + ) + peggedREAL: Optional[float] = Field( + None, + description="Amount pegged to Brazilian Real" + ) + + +class ChainData(BaseModel): + """Model representing stablecoin data for a single chain.""" + + gecko_id: Optional[str] = Field( + None, + description="CoinGecko ID of the chain" + ) + totalCirculatingUSD: CirculatingUSD = Field( + ..., + description="Total circulating amounts in different pegs" + ) + tokenSymbol: Optional[str] = Field( + None, + description="Native token symbol" + ) + name: str = Field( + ..., + description="Chain name" + ) + + +class FetchStablecoinChainsResponse(BaseModel): + """Response schema for stablecoin chains data.""" + + chains: List[ChainData] = Field( + default_factory=list, + description="List of chains with their stablecoin data" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchStablecoinChains(DefiLlamaBaseTool): + """Tool for fetching stablecoin distribution across chains from DeFi Llama. + + This tool retrieves data about how stablecoins are distributed across different + blockchain networks, including circulation amounts and token information. + + Example: + chains_tool = DefiLlamaFetchStablecoinChains( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await chains_tool._arun() + """ + + name: str = "defillama_fetch_stablecoin_chains" + description: str = FETCH_STABLECOIN_CHAINS_PROMPT + args_schema: None = None # No input parameters needed + + def _run(self) -> FetchStablecoinChainsResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self) -> FetchStablecoinChainsResponse: + """Fetch stablecoin distribution data across chains. + + Returns: + FetchStablecoinChainsResponse containing chain data or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchStablecoinChainsResponse(error=error_msg) + + # Fetch chains data from API + result = await fetch_stablecoin_chains() + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchStablecoinChainsResponse(error=result["error"]) + + # Return the response matching the API structure + return FetchStablecoinChainsResponse(chains=result) + + except Exception as e: + return FetchStablecoinChainsResponse(error=str(e)) diff --git a/skills/defillama/stablecoins/fetch_stablecoin_charts.py b/skills/defillama/stablecoins/fetch_stablecoin_charts.py new file mode 100644 index 0000000..e306617 --- /dev/null +++ b/skills/defillama/stablecoins/fetch_stablecoin_charts.py @@ -0,0 +1,150 @@ +"""Tool for fetching stablecoin charts via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_stablecoin_charts + +FETCH_STABLECOIN_CHARTS_PROMPT = """ +This tool fetches historical circulating supply data from DeFi Llama for a specific stablecoin. +Required: +- Stablecoin ID +Optional: +- Chain name for chain-specific data +Returns historical data including: +- Total circulating supply +- Circulating supply in USD +- Daily data points +""" + + +class CirculatingSupply(BaseModel): + """Model representing circulating supply amounts.""" + + peggedUSD: float = Field( + ..., + description="Amount pegged to USD" + ) + + +class StablecoinDataPoint(BaseModel): + """Model representing a single historical data point.""" + + date: str = Field( + ..., + description="Unix timestamp of the data point" + ) + totalCirculating: CirculatingSupply = Field( + ..., + description="Total circulating supply" + ) + totalCirculatingUSD: CirculatingSupply = Field( + ..., + description="Total circulating supply in USD" + ) + + +class FetchStablecoinChartsInput(BaseModel): + """Input schema for fetching stablecoin chart data.""" + + stablecoin_id: str = Field( + ..., + description="ID of the stablecoin to fetch data for" + ) + chain: Optional[str] = Field( + None, + description="Optional chain name for chain-specific data" + ) + + +class FetchStablecoinChartsResponse(BaseModel): + """Response schema for stablecoin chart data.""" + + data: List[StablecoinDataPoint] = Field( + default_factory=list, + description="List of historical data points" + ) + chain: Optional[str] = Field( + None, + description="Chain name if chain-specific data was requested" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchStablecoinCharts(DefiLlamaBaseTool): + """Tool for fetching stablecoin chart data from DeFi Llama. + + This tool retrieves historical circulating supply data for a specific stablecoin, + optionally filtered by chain. + + Example: + charts_tool = DefiLlamaFetchStablecoinCharts( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + # Get all chains data + result = await charts_tool._arun(stablecoin_id="1") + # Get chain-specific data + result = await charts_tool._arun(stablecoin_id="1", chain="ethereum") + """ + + name: str = "defillama_fetch_stablecoin_charts" + description: str = FETCH_STABLECOIN_CHARTS_PROMPT + args_schema: Type[BaseModel] = FetchStablecoinChartsInput + + def _run( + self, stablecoin_id: str + ) -> FetchStablecoinChartsResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun( + self, stablecoin_id: str + ) -> FetchStablecoinChartsResponse: + """Fetch historical chart data for the given stablecoin. + + Args: + stablecoin_id: ID of the stablecoin to fetch data for + chain: Optional chain name for chain-specific data + + Returns: + FetchStablecoinChartsResponse containing historical data or error + """ + try: + # Validate chain if provided + if chain: + is_valid, normalized_chain = await self.validate_chain(chain) + if not is_valid: + return FetchStablecoinChartsResponse( + error=f"Invalid chain: {chain}" + ) + chain = normalized_chain + + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchStablecoinChartsResponse(error=error_msg) + + # Fetch chart data from API + result = await fetch_stablecoin_charts( + stablecoin_id=stablecoin_id, + chain=chain + ) + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchStablecoinChartsResponse(error=result["error"]) + + # Parse response data + return FetchStablecoinChartsResponse( + data=result, + chain=chain + ) + + except Exception as e: + return FetchStablecoinChartsResponse(error=str(e)) diff --git a/skills/defillama/stablecoins/fetch_stablecoin_prices.py b/skills/defillama/stablecoins/fetch_stablecoin_prices.py new file mode 100644 index 0000000..f506099 --- /dev/null +++ b/skills/defillama/stablecoins/fetch_stablecoin_prices.py @@ -0,0 +1,95 @@ +"""Tool for fetching stablecoin prices via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_stablecoin_prices + +FETCH_STABLECOIN_PRICES_PROMPT = """ +This tool fetches current price data for stablecoins from DeFi Llama. +Returns: +- Historical price points with timestamps +- Current prices for each stablecoin +- Prices indexed by stablecoin identifier +""" + + +class PriceDataPoint(BaseModel): + """Model representing a price data point.""" + + date: str = Field( + ..., + description="Unix timestamp for the price data" + ) + prices: Dict[str, float] = Field( + ..., + description="Dictionary of stablecoin prices indexed by identifier" + ) + + +class FetchStablecoinPricesResponse(BaseModel): + """Response schema for stablecoin prices data.""" + + data: List[PriceDataPoint] = Field( + default_factory=list, + description="List of price data points" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchStablecoinPrices(DefiLlamaBaseTool): + """Tool for fetching stablecoin prices from DeFi Llama. + + This tool retrieves current price data for stablecoins, including historical + price points and their timestamps. + + Example: + prices_tool = DefiLlamaFetchStablecoinPrices( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await prices_tool._arun() + """ + + name: str = "defillama_fetch_stablecoin_prices" + description: str = FETCH_STABLECOIN_PRICES_PROMPT + args_schema: None = None # No input parameters needed + + def _run(self) -> FetchStablecoinPricesResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self) -> FetchStablecoinPricesResponse: + """Fetch stablecoin price data. + + Returns: + FetchStablecoinPricesResponse containing price data or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchStablecoinPricesResponse(error=error_msg) + + # Fetch price data from API + result = await fetch_stablecoin_prices() + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchStablecoinPricesResponse(error=result["error"]) + + # Parse results into models + data_points = [ + PriceDataPoint(**point) + for point in result + ] + + return FetchStablecoinPricesResponse(data=data_points) + + except Exception as e: + return FetchStablecoinPricesResponse(error=str(e)) diff --git a/skills/defillama/stablecoins/fetch_stablecoins.py b/skills/defillama/stablecoins/fetch_stablecoins.py new file mode 100644 index 0000000..364474d --- /dev/null +++ b/skills/defillama/stablecoins/fetch_stablecoins.py @@ -0,0 +1,170 @@ +"""Tool for fetching stablecoin data via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_stablecoins + +FETCH_STABLECOINS_PROMPT = """ +This tool fetches comprehensive stablecoin data from DeFi Llama. +Returns: +- List of stablecoins with details like name, symbol, market cap +- Per-chain circulating amounts +- Historical circulating amounts (day/week/month) +- Current prices and price history +- Peg mechanism and type information +""" + + +class CirculatingAmount(BaseModel): + """Model representing circulating amounts for a specific peg type.""" + + peggedUSD: float = Field( + ..., + description="Amount pegged to USD" + ) + + +class ChainCirculating(BaseModel): + """Model representing circulating amounts on a specific chain.""" + + current: CirculatingAmount = Field( + ..., + description="Current circulating amount" + ) + circulatingPrevDay: CirculatingAmount = Field( + ..., + description="Circulating amount from previous day" + ) + circulatingPrevWeek: CirculatingAmount = Field( + ..., + description="Circulating amount from previous week" + ) + circulatingPrevMonth: CirculatingAmount = Field( + ..., + description="Circulating amount from previous month" + ) + + +class Stablecoin(BaseModel): + """Model representing a single stablecoin's data.""" + + id: str = Field( + ..., + description="Unique identifier" + ) + name: str = Field( + ..., + description="Stablecoin name" + ) + symbol: str = Field( + ..., + description="Token symbol" + ) + gecko_id: Optional[str] = Field( + None, + description="CoinGecko ID if available" + ) + pegType: str = Field( + ..., + description="Type of peg (e.g. peggedUSD)" + ) + priceSource: str = Field( + ..., + description="Source of price data" + ) + pegMechanism: str = Field( + ..., + description="Mechanism maintaining the peg" + ) + circulating: CirculatingAmount = Field( + ..., + description="Current total circulating amount" + ) + circulatingPrevDay: CirculatingAmount = Field( + ..., + description="Total circulating amount from previous day" + ) + circulatingPrevWeek: CirculatingAmount = Field( + ..., + description="Total circulating amount from previous week" + ) + circulatingPrevMonth: CirculatingAmount = Field( + ..., + description="Total circulating amount from previous month" + ) + chainCirculating: Dict[str, ChainCirculating] = Field( + ..., + description="Circulating amounts per chain" + ) + chains: List[str] = Field( + ..., + description="List of chains where the stablecoin is present" + ) + price: float = Field( + ..., + description="Current price in USD" + ) + + +class FetchStablecoinsResponse(BaseModel): + """Response schema for stablecoin data.""" + + peggedAssets: List[Stablecoin] = Field( + default_factory=list, + description="List of stablecoins with their data" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchStablecoins(DefiLlamaBaseTool): + """Tool for fetching stablecoin data from DeFi Llama. + + This tool retrieves comprehensive data about stablecoins, including their + circulating supply across different chains, price information, and peg details. + + Example: + stablecoins_tool = DefiLlamaFetchStablecoins( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await stablecoins_tool._arun() + """ + + name: str = "defillama_fetch_stablecoins" + description: str = FETCH_STABLECOINS_PROMPT + args_schema: None = None # No input parameters needed + + def _run(self) -> FetchStablecoinsResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self) -> FetchStablecoinsResponse: + """Fetch stablecoin data. + + Returns: + FetchStablecoinsResponse containing stablecoin data or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchStablecoinsResponse(error=error_msg) + + # Fetch stablecoin data from API + result = await fetch_stablecoins() + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchStablecoinsResponse(error=result["error"]) + + # Return the response matching the API structure + return FetchStablecoinsResponse(**result) + + except Exception as e: + return FetchStablecoinsResponse(error=str(e)) diff --git a/skills/defillama/tests/api_integration.test.py b/skills/defillama/tests/api_integration.test.py new file mode 100644 index 0000000..0911d83 --- /dev/null +++ b/skills/defillama/tests/api_integration.test.py @@ -0,0 +1,182 @@ +import unittest +import asyncio +from datetime import datetime, timedelta +import logging +from unittest.runner import TextTestResult +from unittest.signals import registerResult +import sys + +# Import all functions from your API module +from skills.defillama.api import ( + fetch_protocols, fetch_protocol, fetch_historical_tvl, + fetch_chain_historical_tvl, fetch_protocol_current_tvl, + fetch_chains, fetch_current_prices, fetch_historical_prices, + fetch_batch_historical_prices, fetch_price_chart, + fetch_price_percentage, fetch_first_price, fetch_block, + fetch_stablecoins, fetch_stablecoin_charts, fetch_stablecoin_chains, + fetch_stablecoin_prices, fetch_pools, fetch_pool_chart, + fetch_dex_overview, fetch_dex_summary, fetch_options_overview, + fetch_fees_overview +) + +# Configure logging to only show warnings and errors +logging.basicConfig(level=logging.WARNING) + +class QuietTestResult(TextTestResult): + """Custom TestResult class that minimizes output unless there's a failure""" + def startTest(self, test): + self._started_at = datetime.now() + super().startTest(test) + + def addSuccess(self, test): + super().addSuccess(test) + if self.showAll: + self.stream.write('.') + self.stream.flush() + + def addError(self, test, err): + super().addError(test, err) + self.stream.write('\n') + self.stream.write(self.separator1 + '\n') + self.stream.write(f'ERROR: {self.getDescription(test)}\n') + self.stream.write(self.separator2 + '\n') + self.stream.write(self._exc_info_to_string(err, test)) + self.stream.write('\n') + self.stream.flush() + + def addFailure(self, test, err): + super().addFailure(test, err) + self.stream.write('\n') + self.stream.write(self.separator1 + '\n') + self.stream.write(f'FAIL: {self.getDescription(test)}\n') + self.stream.write(self.separator2 + '\n') + self.stream.write(self._exc_info_to_string(err, test)) + self.stream.write('\n') + self.stream.flush() + +class QuietTestRunner(unittest.TextTestRunner): + """Custom TestRunner that uses QuietTestResult""" + resultclass = QuietTestResult + +class TestDefiLlamaAPI(unittest.TestCase): + """Integration tests for DeFi Llama API client""" + + def setUp(self): + """Set up the async event loop""" + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + self.timeout = 30 + + def tearDown(self): + """Clean up the event loop""" + self.loop.close() + + def run_async(self, coro): + """Helper to run async functions in test methods with timeout""" + try: + return self.loop.run_until_complete( + asyncio.wait_for(coro, timeout=self.timeout) + ) + except asyncio.TimeoutError: + raise AssertionError(f"Test timed out after {self.timeout} seconds") + except Exception as e: + raise AssertionError(f"Test failed with exception: {str(e)}") + + def assert_successful_response(self, response): + """Helper to check if response contains an error""" + if isinstance(response, dict) and "error" in response: + raise AssertionError(f"API request failed: {response['error']}") + + def test_tvl_endpoints(self): + """Test TVL-related endpoints""" + # Test fetch_protocols + protocols = self.run_async(fetch_protocols()) + self.assert_successful_response(protocols) + self.assertIsInstance(protocols, list) + if len(protocols) > 0: + self.assertIn("tvl", protocols[0]) + + # Test fetch_protocol using Aave as an example + protocol_data = self.run_async(fetch_protocol("aave")) + self.assert_successful_response(protocol_data) + self.assertIsInstance(protocol_data, dict) + + # Test fetch_historical_tvl + historical_tvl = self.run_async(fetch_historical_tvl()) + self.assert_successful_response(historical_tvl) + self.assertIsInstance(historical_tvl, list) + # Verify the structure of historical TVL data points + if len(historical_tvl) > 0: + self.assertIn('date', historical_tvl[0]) + self.assertIn('tvl', historical_tvl[0]) + + # Test fetch_chains + chains = self.run_async(fetch_chains()) + self.assert_successful_response(chains) + self.assertIsInstance(chains, list) + + def test_coins_endpoints(self): + """Test coin price-related endpoints""" + test_coins = ["ethereum:0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"] + + # Test fetch_current_prices + current_prices = self.run_async(fetch_current_prices(test_coins)) + self.assert_successful_response(current_prices) + self.assertIsInstance(current_prices, dict) + + # Test fetch_historical_prices + timestamp = int((datetime.now() - timedelta(days=1)).timestamp()) + historical_prices = self.run_async(fetch_historical_prices(timestamp, test_coins)) + self.assert_successful_response(historical_prices) + self.assertIsInstance(historical_prices, dict) + + # Test fetch_price_chart + price_chart = self.run_async(fetch_price_chart(test_coins)) + self.assert_successful_response(price_chart) + self.assertIsInstance(price_chart, dict) + self.assertIn('coins', price_chart) + # Verify the structure of the response + coin_data = price_chart['coins'].get(test_coins[0]) + self.assertIsNotNone(coin_data) + self.assertIn('prices', coin_data) + self.assertIsInstance(coin_data['prices'], list) + + def test_stablecoin_endpoints(self): + """Test stablecoin-related endpoints""" + # Test fetch_stablecoins + stablecoins = self.run_async(fetch_stablecoins()) + self.assert_successful_response(stablecoins) + self.assertIsInstance(stablecoins, dict) + + # Test fetch_stablecoin_chains + chains = self.run_async(fetch_stablecoin_chains()) + self.assert_successful_response(chains) + self.assertIsInstance(chains, list) + + # Test fetch_stablecoin_prices + prices = self.run_async(fetch_stablecoin_prices()) + self.assert_successful_response(prices) + self.assertIsInstance(prices, list) + + def test_volume_endpoints(self): + """Test volume-related endpoints""" + # Test fetch_dex_overview + dex_overview = self.run_async(fetch_dex_overview()) + self.assert_successful_response(dex_overview) + self.assertIsInstance(dex_overview, dict) + + # Test fetch_dex_summary using Uniswap as example + dex_summary = self.run_async(fetch_dex_summary("uniswap")) + self.assert_successful_response(dex_summary) + self.assertIsInstance(dex_summary, dict) + + def test_fees_endpoint(self): + """Test fees endpoint""" + fees_overview = self.run_async(fetch_fees_overview()) + self.assert_successful_response(fees_overview) + self.assertIsInstance(fees_overview, dict) + +if __name__ == "__main__": + # Use the quiet test runner + runner = QuietTestRunner(verbosity=1) + unittest.main(testRunner=runner) diff --git a/skills/defillama/tests/api_unit.test.py b/skills/defillama/tests/api_unit.test.py new file mode 100644 index 0000000..5faa73c --- /dev/null +++ b/skills/defillama/tests/api_unit.test.py @@ -0,0 +1,619 @@ +import unittest +from unittest.mock import patch, AsyncMock +import asyncio + +# Import the endpoints from your module. +# Adjust the import path if your module has a different name or location. +from skills.defillama.api import ( + # Original functions + fetch_protocols, + fetch_protocol, + fetch_historical_tvl, + fetch_chain_historical_tvl, + fetch_protocol_current_tvl, + fetch_chains, + fetch_current_prices, + fetch_historical_prices, + fetch_batch_historical_prices, + # Price related functions + fetch_price_chart, + fetch_price_percentage, + fetch_first_price, + fetch_block, + # Stablecoin related functions + fetch_stablecoins, + fetch_stablecoin_charts, + fetch_stablecoin_chains, + fetch_stablecoin_prices, + # Yields related functions + fetch_pools, + fetch_pool_chart, + # Volume related functions + fetch_dex_overview, + fetch_dex_summary, + fetch_options_overview, + # Fees related functions + fetch_fees_overview, +) +# Dummy response to simulate httpx responses. +class DummyResponse: + def __init__(self, status_code, json_data): + self.status_code = status_code + self._json_data = json_data + + def json(self): + return self._json_data + +class TestDefiLlamaAPI(unittest.IsolatedAsyncioTestCase): + + @classmethod + def setUpClass(cls): + # Set up a fixed timestamp that all tests will use + cls.mock_timestamp = 1677648000 # Fixed timestamp + + async def asyncSetUp(self): + # Start the patcher before each test + self.datetime_patcher = patch('skills.defillama.api.datetime') + self.mock_datetime = self.datetime_patcher.start() + # Configure the mock to return our fixed timestamp + self.mock_datetime.now.return_value.timestamp.return_value = self.mock_timestamp + + async def asyncTearDown(self): + # Stop the patcher after each test + self.datetime_patcher.stop() + + # Helper method to patch httpx.AsyncClient and set up the dummy client. + async def _run_with_dummy(self, func, expected_url, dummy_response, *args, expected_kwargs=None): + if expected_kwargs is None: + expected_kwargs = {} + with patch("httpx.AsyncClient") as MockClient: + client_instance = AsyncMock() + client_instance.get.return_value = dummy_response + # Ensure that __aenter__ returns our dummy client. + MockClient.return_value.__aenter__.return_value = client_instance + result = await func(*args) + # Check that the get call was made with the expected URL (and parameters, if any). + client_instance.get.assert_called_once_with(expected_url, **expected_kwargs) + return result + + # --- Tests for fetch_protocols --- + async def test_fetch_protocols_success(self): + dummy = DummyResponse(200, {"protocols": []}) + result = await self._run_with_dummy( + fetch_protocols, + "https://api.llama.fi/protocols", + dummy, + ) + self.assertEqual(result, {"protocols": []}) + + async def test_fetch_protocols_error(self): + dummy = DummyResponse(404, None) + result = await self._run_with_dummy( + fetch_protocols, + "https://api.llama.fi/protocols", + dummy, + ) + self.assertEqual(result, {"error": "API returned status code 404"}) + + # --- Tests for fetch_protocol --- + async def test_fetch_protocol_success(self): + protocol = "testprotocol" + dummy = DummyResponse(200, {"protocol": protocol}) + expected_url = f"https://api.llama.fi/protocol/{protocol}" + result = await self._run_with_dummy( + fetch_protocol, + expected_url, + dummy, + protocol + ) + self.assertEqual(result, {"protocol": protocol}) + + async def test_fetch_protocol_error(self): + protocol = "testprotocol" + dummy = DummyResponse(500, None) + expected_url = f"https://api.llama.fi/protocol/{protocol}" + result = await self._run_with_dummy( + fetch_protocol, + expected_url, + dummy, + protocol + ) + self.assertEqual(result, {"error": "API returned status code 500"}) + + # --- Tests for fetch_historical_tvl --- + async def test_fetch_historical_tvl_success(self): + dummy = DummyResponse(200, {"historical": "data"}) + expected_url = "https://api.llama.fi/v2/historicalChainTvl" + result = await self._run_with_dummy( + fetch_historical_tvl, + expected_url, + dummy, + ) + self.assertEqual(result, {"historical": "data"}) + + async def test_fetch_historical_tvl_error(self): + dummy = DummyResponse(400, None) + expected_url = "https://api.llama.fi/v2/historicalChainTvl" + result = await self._run_with_dummy( + fetch_historical_tvl, + expected_url, + dummy, + ) + self.assertEqual(result, {"error": "API returned status code 400"}) + + # --- Tests for fetch_chain_historical_tvl --- + async def test_fetch_chain_historical_tvl_success(self): + chain = "ethereum" + dummy = DummyResponse(200, {"chain": chain}) + expected_url = f"https://api.llama.fi/v2/historicalChainTvl/{chain}" + result = await self._run_with_dummy( + fetch_chain_historical_tvl, + expected_url, + dummy, + chain + ) + self.assertEqual(result, {"chain": chain}) + + async def test_fetch_chain_historical_tvl_error(self): + chain = "ethereum" + dummy = DummyResponse(503, None) + expected_url = f"https://api.llama.fi/v2/historicalChainTvl/{chain}" + result = await self._run_with_dummy( + fetch_chain_historical_tvl, + expected_url, + dummy, + chain + ) + self.assertEqual(result, {"error": "API returned status code 503"}) + + # --- Tests for fetch_protocol_current_tvl --- + async def test_fetch_protocol_current_tvl_success(self): + protocol = "testprotocol" + dummy = DummyResponse(200, {"current_tvl": 12345}) + expected_url = f"https://api.llama.fi/tvl/{protocol}" + result = await self._run_with_dummy( + fetch_protocol_current_tvl, + expected_url, + dummy, + protocol + ) + self.assertEqual(result, {"current_tvl": 12345}) + + async def test_fetch_protocol_current_tvl_error(self): + protocol = "testprotocol" + dummy = DummyResponse(418, None) + expected_url = f"https://api.llama.fi/tvl/{protocol}" + result = await self._run_with_dummy( + fetch_protocol_current_tvl, + expected_url, + dummy, + protocol + ) + self.assertEqual(result, {"error": "API returned status code 418"}) + + # --- Tests for fetch_chains --- + async def test_fetch_chains_success(self): + dummy = DummyResponse(200, {"chains": ["eth", "bsc"]}) + expected_url = "https://api.llama.fi/v2/chains" + result = await self._run_with_dummy( + fetch_chains, + expected_url, + dummy, + ) + self.assertEqual(result, {"chains": ["eth", "bsc"]}) + + async def test_fetch_chains_error(self): + dummy = DummyResponse(404, None) + expected_url = "https://api.llama.fi/v2/chains" + result = await self._run_with_dummy( + fetch_chains, + expected_url, + dummy, + ) + self.assertEqual(result, {"error": "API returned status code 404"}) + + # --- Tests for fetch_current_prices --- + async def test_fetch_current_prices_success(self): + coins = ["coin1", "coin2"] + coins_str = ",".join(coins) + dummy = DummyResponse(200, {"prices": "data"}) + expected_url = f"https://api.llama.fi/prices/current/{coins_str}?searchWidth=4h" + result = await self._run_with_dummy( + fetch_current_prices, + expected_url, + dummy, + coins + ) + self.assertEqual(result, {"prices": "data"}) + + async def test_fetch_current_prices_error(self): + coins = ["coin1", "coin2"] + coins_str = ",".join(coins) + dummy = DummyResponse(500, None) + expected_url = f"https://api.llama.fi/prices/current/{coins_str}?searchWidth=4h" + result = await self._run_with_dummy( + fetch_current_prices, + expected_url, + dummy, + coins + ) + self.assertEqual(result, {"error": "API returned status code 500"}) + + # --- Tests for fetch_historical_prices --- + async def test_fetch_historical_prices_success(self): + timestamp = 1609459200 + coins = ["coin1", "coin2"] + coins_str = ",".join(coins) + dummy = DummyResponse(200, {"historical_prices": "data"}) + expected_url = f"https://api.llama.fi/prices/historical/{timestamp}/{coins_str}?searchWidth=4h" + result = await self._run_with_dummy( + fetch_historical_prices, + expected_url, + dummy, + timestamp, + coins + ) + self.assertEqual(result, {"historical_prices": "data"}) + + async def test_fetch_historical_prices_error(self): + timestamp = 1609459200 + coins = ["coin1", "coin2"] + coins_str = ",".join(coins) + dummy = DummyResponse(400, None) + expected_url = f"https://api.llama.fi/prices/historical/{timestamp}/{coins_str}?searchWidth=4h" + result = await self._run_with_dummy( + fetch_historical_prices, + expected_url, + dummy, + timestamp, + coins + ) + self.assertEqual(result, {"error": "API returned status code 400"}) + + # --- Tests for fetch_batch_historical_prices --- + async def test_fetch_batch_historical_prices_success(self): + coins_timestamps = {"coin1": [1609459200, 1609545600], "coin2": [1609459200]} + dummy = DummyResponse(200, {"batch": "data"}) + expected_url = "https://api.llama.fi/batchHistorical" + # For this endpoint, a params dict is sent. + expected_params = {"coins": coins_timestamps, "searchWidth": "600"} + with patch("httpx.AsyncClient") as MockClient: + client_instance = AsyncMock() + client_instance.get.return_value = dummy + MockClient.return_value.__aenter__.return_value = client_instance + result = await fetch_batch_historical_prices(coins_timestamps) + client_instance.get.assert_called_once_with(expected_url, params=expected_params) + self.assertEqual(result, {"batch": "data"}) + + async def test_fetch_batch_historical_prices_error(self): + coins_timestamps = {"coin1": [1609459200], "coin2": [1609459200]} + dummy = DummyResponse(503, None) + expected_url = "https://api.llama.fi/batchHistorical" + expected_params = {"coins": coins_timestamps, "searchWidth": "600"} + with patch("httpx.AsyncClient") as MockClient: + client_instance = AsyncMock() + client_instance.get.return_value = dummy + MockClient.return_value.__aenter__.return_value = client_instance + result = await fetch_batch_historical_prices(coins_timestamps) + client_instance.get.assert_called_once_with(expected_url, params=expected_params) + self.assertEqual(result, {"error": "API returned status code 503"}) + + async def test_fetch_price_chart_success(self): + coins = ["bitcoin", "ethereum"] + coins_str = ",".join(coins) + dummy = DummyResponse(200, {"chart": "data"}) + expected_url = f"https://api.llama.fi/chart/{coins_str}" + + # Calculate start time based on mock timestamp + start_time = self.mock_timestamp - 86400 # mock timestamp - 1 day + expected_params = { + "start": start_time, + "span": 10, + "period": "2d", + "searchWidth": "600" + } + + with patch("httpx.AsyncClient") as MockClient: + client_instance = AsyncMock() + client_instance.get.return_value = dummy + MockClient.return_value.__aenter__.return_value = client_instance + result = await fetch_price_chart(coins) + client_instance.get.assert_called_once_with(expected_url, params=expected_params) + self.assertEqual(result, {"chart": "data"}) + + async def test_fetch_price_chart_error(self): + coins = ["bitcoin", "ethereum"] + coins_str = ",".join(coins) + dummy = DummyResponse(500, None) + expected_url = f"https://api.llama.fi/chart/{coins_str}" + + # Calculate start time based on mock timestamp + start_time = self.mock_timestamp - 86400 # mock timestamp - 1 day + expected_params = { + "start": start_time, + "span": 10, + "period": "2d", + "searchWidth": "600" + } + + with patch("httpx.AsyncClient") as MockClient: + client_instance = AsyncMock() + client_instance.get.return_value = dummy + MockClient.return_value.__aenter__.return_value = client_instance + result = await fetch_price_chart(coins) + client_instance.get.assert_called_once_with(expected_url, params=expected_params) + self.assertEqual(result, {"error": "API returned status code 500"}) + + + # --- Tests for fetch_price_percentage --- + async def test_fetch_price_percentage_success(self): + coins = ["bitcoin", "ethereum"] + coins_str = ",".join(coins) + dummy = DummyResponse(200, {"percentage": "data"}) + expected_url = f"https://api.llama.fi/percentage/{coins_str}" + + mock_timestamp = 1677648000 # Fixed timestamp + with patch("skills.defillama.api.datetime") as mock_datetime: + mock_datetime.now.return_value.timestamp.return_value = mock_timestamp + expected_params = { + "timestamp": mock_timestamp, + "lookForward": "false", + "period": "24h" + } + + with patch("httpx.AsyncClient") as MockClient: + client_instance = AsyncMock() + client_instance.get.return_value = dummy + MockClient.return_value.__aenter__.return_value = client_instance + result = await fetch_price_percentage(coins) + client_instance.get.assert_called_once_with(expected_url, params=expected_params) + self.assertEqual(result, {"percentage": "data"}) + + async def test_fetch_price_percentage_error(self): + coins = ["bitcoin", "ethereum"] + coins_str = ",".join(coins) + dummy = DummyResponse(404, None) + expected_url = f"https://api.llama.fi/percentage/{coins_str}" + + expected_params = { + "timestamp": self.mock_timestamp, + "lookForward": "false", + "period": "24h" + } + + with patch("httpx.AsyncClient") as MockClient: + client_instance = AsyncMock() + client_instance.get.return_value = dummy + MockClient.return_value.__aenter__.return_value = client_instance + result = await fetch_price_percentage(coins) + client_instance.get.assert_called_once_with(expected_url, params=expected_params) + self.assertEqual(result, {"error": "API returned status code 404"}) + + + async def test_fetch_price_percentage_error(self): + coins = ["bitcoin", "ethereum"] + coins_str = ",".join(coins) + dummy = DummyResponse(404, None) + expected_url = f"https://api.llama.fi/percentage/{coins_str}" + + with patch("datetime.datetime") as mock_datetime: + mock_datetime.now.return_value.timestamp.return_value = 1677648000 + expected_params = { + "timestamp": 1677648000, + "lookForward": "false", + "period": "24h" + } + + with patch("httpx.AsyncClient") as MockClient: + client_instance = AsyncMock() + client_instance.get.return_value = dummy + MockClient.return_value.__aenter__.return_value = client_instance + result = await fetch_price_percentage(coins) + client_instance.get.assert_called_once_with(expected_url, params=expected_params) + self.assertEqual(result, {"error": "API returned status code 404"}) + + # --- Tests for fetch_first_price --- + async def test_fetch_first_price_success(self): + coins = ["bitcoin", "ethereum"] + coins_str = ",".join(coins) + dummy = DummyResponse(200, {"first_prices": "data"}) + expected_url = f"https://api.llama.fi/prices/first/{coins_str}" + result = await self._run_with_dummy( + fetch_first_price, + expected_url, + dummy, + coins + ) + self.assertEqual(result, {"first_prices": "data"}) + + async def test_fetch_first_price_error(self): + coins = ["bitcoin", "ethereum"] + coins_str = ",".join(coins) + dummy = DummyResponse(500, None) + expected_url = f"https://api.llama.fi/prices/first/{coins_str}" + result = await self._run_with_dummy( + fetch_first_price, + expected_url, + dummy, + coins + ) + self.assertEqual(result, {"error": "API returned status code 500"}) + + # --- Tests for fetch_block --- + async def test_fetch_block_success(self): + chain = "ethereum" + dummy = DummyResponse(200, {"block": 123456}) + mock_timestamp = 1677648000 # Fixed timestamp + + with patch("skills.defillama.api.datetime") as mock_datetime: + mock_datetime.now.return_value.timestamp.return_value = mock_timestamp + expected_url = f"https://api.llama.fi/block/{chain}/{mock_timestamp}" + result = await self._run_with_dummy( + fetch_block, + expected_url, + dummy, + chain + ) + self.assertEqual(result, {"block": 123456}) + + async def test_fetch_block_error(self): + chain = "ethereum" + dummy = DummyResponse(404, None) + mock_timestamp = 1677648000 # Fixed timestamp + + with patch("skills.defillama.api.datetime") as mock_datetime: + mock_datetime.now.return_value.timestamp.return_value = mock_timestamp + expected_url = f"https://api.llama.fi/block/{chain}/{mock_timestamp}" + result = await self._run_with_dummy( + fetch_block, + expected_url, + dummy, + chain + ) + self.assertEqual(result, {"error": "API returned status code 404"}) + + # --- Tests for Stablecoins API --- + async def test_fetch_stablecoins_success(self): + dummy = DummyResponse(200, {"stablecoins": "data"}) + expected_url = "https://api.llama.fi/stablecoins" + expected_params = {"includePrices": "true"} + + with patch("httpx.AsyncClient") as MockClient: + client_instance = AsyncMock() + client_instance.get.return_value = dummy + MockClient.return_value.__aenter__.return_value = client_instance + result = await fetch_stablecoins() + client_instance.get.assert_called_once_with(expected_url, params=expected_params) + self.assertEqual(result, {"stablecoins": "data"}) + + async def test_fetch_stablecoin_charts_success(self): + stablecoin_id = "USDT" + chain = "ethereum" + dummy = DummyResponse(200, {"charts": "data"}) + expected_url = f"https://api.llama.fi/stablecoincharts/{chain}?stablecoin={stablecoin_id}" + result = await self._run_with_dummy( + fetch_stablecoin_charts, + expected_url, + dummy, + stablecoin_id, + chain + ) + self.assertEqual(result, {"charts": "data"}) + + async def test_fetch_stablecoin_chains_success(self): + dummy = DummyResponse(200, {"chains": "data"}) + expected_url = "https://api.llama.fi/stablecoinchains" + result = await self._run_with_dummy( + fetch_stablecoin_chains, + expected_url, + dummy + ) + self.assertEqual(result, {"chains": "data"}) + + async def test_fetch_stablecoin_prices_success(self): + dummy = DummyResponse(200, {"prices": "data"}) + expected_url = "https://api.llama.fi/stablecoinprices" + result = await self._run_with_dummy( + fetch_stablecoin_prices, + expected_url, + dummy + ) + self.assertEqual(result, {"prices": "data"}) + + # --- Tests for Yields API --- + async def test_fetch_pools_success(self): + dummy = DummyResponse(200, {"pools": "data"}) + expected_url = "https://api.llama.fi/pools" + result = await self._run_with_dummy( + fetch_pools, + expected_url, + dummy + ) + self.assertEqual(result, {"pools": "data"}) + + async def test_fetch_pool_chart_success(self): + pool_id = "compound-usdc" + dummy = DummyResponse(200, {"chart": "data"}) + expected_url = f"https://api.llama.fi/chart/{pool_id}" + result = await self._run_with_dummy( + fetch_pool_chart, + expected_url, + dummy, + pool_id + ) + self.assertEqual(result, {"chart": "data"}) + + # --- Tests for Volumes API --- + async def test_fetch_dex_overview_success(self): + dummy = DummyResponse(200, {"overview": "data"}) + expected_url = "https://api.llama.fi/overview/dexs" + expected_params = { + "excludeTotalDataChart": "true", + "excludeTotalDataChartBreakdown": "true", + "dataType": "dailyVolume" + } + + with patch("httpx.AsyncClient") as MockClient: + client_instance = AsyncMock() + client_instance.get.return_value = dummy + MockClient.return_value.__aenter__.return_value = client_instance + result = await fetch_dex_overview() + client_instance.get.assert_called_once_with(expected_url, params=expected_params) + self.assertEqual(result, {"overview": "data"}) + + async def test_fetch_dex_summary_success(self): + protocol = "uniswap" + dummy = DummyResponse(200, {"summary": "data"}) + expected_url = f"https://api.llama.fi/summary/dexs/{protocol}" + expected_params = { + "excludeTotalDataChart": "true", + "excludeTotalDataChartBreakdown": "true", + "dataType": "dailyVolume" + } + + with patch("httpx.AsyncClient") as MockClient: + client_instance = AsyncMock() + client_instance.get.return_value = dummy + MockClient.return_value.__aenter__.return_value = client_instance + result = await fetch_dex_summary(protocol) + client_instance.get.assert_called_once_with(expected_url, params=expected_params) + self.assertEqual(result, {"summary": "data"}) + + async def test_fetch_options_overview_success(self): + dummy = DummyResponse(200, {"options": "data"}) + expected_url = "https://api.llama.fi/overview/options" + expected_params = { + "excludeTotalDataChart": "true", + "excludeTotalDataChartBreakdown": "true", + "dataType": "dailyPremiumVolume" + } + + with patch("httpx.AsyncClient") as MockClient: + client_instance = AsyncMock() + client_instance.get.return_value = dummy + MockClient.return_value.__aenter__.return_value = client_instance + result = await fetch_options_overview() + client_instance.get.assert_called_once_with(expected_url, params=expected_params) + self.assertEqual(result, {"options": "data"}) + + # --- Tests for Fees API --- + async def test_fetch_fees_overview_success(self): + dummy = DummyResponse(200, {"fees": "data"}) + expected_url = "https://api.llama.fi/overview/fees" + expected_params = { + "excludeTotalDataChart": "true", + "excludeTotalDataChartBreakdown": "true", + "dataType": "dailyFees" + } + + with patch("httpx.AsyncClient") as MockClient: + client_instance = AsyncMock() + client_instance.get.return_value = dummy + MockClient.return_value.__aenter__.return_value = client_instance + result = await fetch_fees_overview() + client_instance.get.assert_called_once_with(expected_url, params=expected_params) + self.assertEqual(result, {"fees": "data"}) + +if __name__ == '__main__': + unittest.main() + diff --git a/skills/defillama/fetch_chain_historical_tvl.py b/skills/defillama/tvl/fetch_chain_historical_tvl.py similarity index 100% rename from skills/defillama/fetch_chain_historical_tvl.py rename to skills/defillama/tvl/fetch_chain_historical_tvl.py diff --git a/skills/defillama/fetch_chains.py b/skills/defillama/tvl/fetch_chains.py similarity index 100% rename from skills/defillama/fetch_chains.py rename to skills/defillama/tvl/fetch_chains.py diff --git a/skills/defillama/fetch_historical_tvl.py b/skills/defillama/tvl/fetch_historical_tvl.py similarity index 100% rename from skills/defillama/fetch_historical_tvl.py rename to skills/defillama/tvl/fetch_historical_tvl.py diff --git a/skills/defillama/fetch_protocol.py b/skills/defillama/tvl/fetch_protocol.py similarity index 99% rename from skills/defillama/fetch_protocol.py rename to skills/defillama/tvl/fetch_protocol.py index 76e534d..f545ad9 100644 --- a/skills/defillama/fetch_protocol.py +++ b/skills/defillama/tvl/fetch_protocol.py @@ -1,6 +1,6 @@ """Tool for fetching specific protocol details via DeFi Llama API.""" -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Union, Type from datetime import datetime from pydantic import BaseModel, Field diff --git a/skills/defillama/fetch_protocol_current_tvl.py b/skills/defillama/tvl/fetch_protocol_current_tvl.py similarity index 100% rename from skills/defillama/fetch_protocol_current_tvl.py rename to skills/defillama/tvl/fetch_protocol_current_tvl.py diff --git a/skills/defillama/fetch_protocols.py b/skills/defillama/tvl/fetch_protocols.py similarity index 99% rename from skills/defillama/fetch_protocols.py rename to skills/defillama/tvl/fetch_protocols.py index f885173..34c6ba8 100644 --- a/skills/defillama/fetch_protocols.py +++ b/skills/defillama/tvl/fetch_protocols.py @@ -1,6 +1,6 @@ """Tool for fetching all protocols via DeFi Llama API.""" -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Union, Type from pydantic import BaseModel, Field from skills.defillama.base import DefiLlamaBaseTool diff --git a/skills/defillama/volumes/fetch_dex_overview.py b/skills/defillama/volumes/fetch_dex_overview.py new file mode 100644 index 0000000..3605a51 --- /dev/null +++ b/skills/defillama/volumes/fetch_dex_overview.py @@ -0,0 +1,212 @@ +"""Tool for fetching DEX overview data via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_dex_overview + +FETCH_DEX_OVERVIEW_PROMPT = """ +This tool fetches comprehensive overview data for DEX protocols from DeFi Llama. +Returns: +- Chain statistics and breakdowns +- Protocol-specific metrics +- Change percentages +- Total volume data +""" + + +class MethodologyInfo(BaseModel): + """Model representing methodology information.""" + + UserFees: Optional[str] = Field(None, description="User fee information") + Fees: Optional[str] = Field(None, description="Fee structure") + Revenue: Optional[str] = Field(None, description="Revenue model") + ProtocolRevenue: Optional[str] = Field(None, description="Protocol revenue info") + HoldersRevenue: Optional[str] = Field(None, description="Holder revenue info") + SupplySideRevenue: Optional[str] = Field(None, description="Supply side revenue info") + + +class ProtocolInfo(BaseModel): + """Model representing individual protocol data.""" + + total24h: Optional[float] = Field(None, description="24h total") + total48hto24h: Optional[float] = Field(None, description="48h to 24h total") + total7d: Optional[float] = Field(None, description="7d total") + total14dto7d: Optional[float] = Field(None, description="14d to 7d total") + total60dto30d: Optional[float] = Field(None, description="60d to 30d total") + total30d: Optional[float] = Field(None, description="30d total") + total1y: Optional[float] = Field(None, description="1y total") + totalAllTime: Optional[float] = Field(None, description="All time total") + average1y: Optional[float] = Field(None, description="1y average") + change_1d: Optional[float] = Field(None, description="1d change") + change_7d: Optional[float] = Field(None, description="7d change") + change_1m: Optional[float] = Field(None, description="1m change") + change_7dover7d: Optional[float] = Field(None, description="7d over 7d change") + change_30dover30d: Optional[float] = Field(None, description="30d over 30d change") + breakdown24h: Optional[Dict[str, Dict[str, float]]] = Field( + None, description="24h breakdown by chain" + ) + breakdown30d: Optional[Dict[str, Dict[str, float]]] = Field( + None, description="30d breakdown by chain" + ) + total7DaysAgo: Optional[float] = Field(None, description="Total 7 days ago") + total30DaysAgo: Optional[float] = Field(None, description="Total 30 days ago") + defillamaId: Optional[str] = Field(None, description="DeFi Llama ID") + name: str = Field(..., description="Protocol name") + displayName: str = Field(..., description="Display name") + module: str = Field(..., description="Module name") + category: str = Field(..., description="Protocol category") + logo: Optional[str] = Field(None, description="Logo URL") + chains: List[str] = Field(..., description="Supported chains") + protocolType: str = Field(..., description="Protocol type") + methodologyURL: Optional[str] = Field(None, description="Methodology URL") + methodology: Optional[MethodologyInfo] = Field(None, description="Methodology details") + latestFetchIsOk: bool = Field(..., description="Latest fetch status") + disabled: Optional[bool] = Field(None, description="Whether protocol is disabled") + parentProtocol: Optional[str] = Field(None, description="Parent protocol") + slug: str = Field(..., description="Protocol slug") + linkedProtocols: Optional[List[str]] = Field(None, description="Linked protocols") + id: str = Field(..., description="Protocol ID") + + +class FetchDexOverviewResponse(BaseModel): + """Response schema for DEX overview data.""" + + totalDataChart: List = Field( + default_factory=list, + description="Total data chart points" + ) + totalDataChartBreakdown: List = Field( + default_factory=list, + description="Total data chart breakdown" + ) + breakdown24h: Optional[Dict[str, Dict[str, float]]] = Field( + None, + description="24h breakdown by chain" + ) + breakdown30d: Optional[Dict[str, Dict[str, float]]] = Field( + None, + description="30d breakdown by chain" + ) + chain: Optional[str] = Field( + None, + description="Specific chain" + ) + allChains: List[str] = Field( + ..., + description="List of all chains" + ) + total24h: float = Field( + ..., + description="24h total" + ) + total48hto24h: float = Field( + ..., + description="48h to 24h total" + ) + total7d: float = Field( + ..., + description="7d total" + ) + total14dto7d: float = Field( + ..., + description="14d to 7d total" + ) + total60dto30d: float = Field( + ..., + description="60d to 30d total" + ) + total30d: float = Field( + ..., + description="30d total" + ) + total1y: float = Field( + ..., + description="1y total" + ) + change_1d: float = Field( + ..., + description="1d change" + ) + change_7d: float = Field( + ..., + description="7d change" + ) + change_1m: float = Field( + ..., + description="1m change" + ) + change_7dover7d: float = Field( + ..., + description="7d over 7d change" + ) + change_30dover30d: float = Field( + ..., + description="30d over 30d change" + ) + total7DaysAgo: float = Field( + ..., + description="Total 7 days ago" + ) + total30DaysAgo: float = Field( + ..., + description="Total 30 days ago" + ) + protocols: List[ProtocolInfo] = Field( + ..., + description="List of protocol data" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchDexOverview(DefiLlamaBaseTool): + """Tool for fetching DEX overview data from DeFi Llama. + + This tool retrieves comprehensive data about DEX protocols, including + volumes, metrics, and chain breakdowns. + + Example: + overview_tool = DefiLlamaFetchDexOverview( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await overview_tool._arun() + """ + + name: str = "defillama_fetch_dex_overview" + description: str = FETCH_DEX_OVERVIEW_PROMPT + args_schema: None = None # No input parameters needed + + def _run(self) -> FetchDexOverviewResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self) -> FetchDexOverviewResponse: + """Fetch DEX overview data. + + Returns: + FetchDexOverviewResponse containing overview data or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchDexOverviewResponse(error=error_msg) + + # Fetch overview data from API + result = await fetch_dex_overview() + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchDexOverviewResponse(error=result["error"]) + + # Return the response matching the API structure + return FetchDexOverviewResponse(**result) + + except Exception as e: + return FetchDexOverviewResponse(error=str(e)) diff --git a/skills/defillama/volumes/fetch_dex_summary.py b/skills/defillama/volumes/fetch_dex_summary.py new file mode 100644 index 0000000..7f62d94 --- /dev/null +++ b/skills/defillama/volumes/fetch_dex_summary.py @@ -0,0 +1,126 @@ +"""Tool for fetching DEX protocol summary data via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_dex_summary + +FETCH_DEX_SUMMARY_PROMPT = """ +This tool fetches summary data for a specific DEX protocol from DeFi Llama. +Required: +- Protocol identifier +Returns: +- Protocol details and metadata +- Volume metrics +- Social links and identifiers +- Child protocols and versions +""" + + +class FetchDexSummaryInput(BaseModel): + """Input schema for fetching DEX protocol summary.""" + + protocol: str = Field( + ..., + description="Protocol identifier (e.g. 'uniswap')" + ) + + +class FetchDexSummaryResponse(BaseModel): + """Response schema for DEX protocol summary data.""" + + id: str = Field(..., description="Protocol ID") + name: str = Field(..., description="Protocol name") + url: Optional[str] = Field(None, description="Protocol website URL") + description: Optional[str] = Field(None, description="Protocol description") + logo: Optional[str] = Field(None, description="Logo URL") + gecko_id: Optional[str] = Field(None, description="CoinGecko ID") + cmcId: Optional[str] = Field(None, description="CoinMarketCap ID") + chains: List[str] = Field(default_factory=list, description="Supported chains") + twitter: Optional[str] = Field(None, description="Twitter handle") + treasury: Optional[str] = Field(None, description="Treasury identifier") + governanceID: Optional[List[str]] = Field(None, description="Governance IDs") + github: Optional[List[str]] = Field(None, description="GitHub organizations") + childProtocols: Optional[List[str]] = Field(None, description="Child protocols") + linkedProtocols: Optional[List[str]] = Field(None, description="Linked protocols") + disabled: Optional[bool] = Field(None, description="Whether protocol is disabled") + displayName: str = Field(..., description="Display name") + module: Optional[str] = Field(None, description="Module name") + category: Optional[str] = Field(None, description="Protocol category") + methodologyURL: Optional[str] = Field(None, description="Methodology URL") + methodology: Optional[Dict] = Field(None, description="Methodology details") + forkedFrom: Optional[List[str]] = Field(None, description="Forked from protocols") + audits: Optional[str] = Field(None, description="Audit information") + address: Optional[str] = Field(None, description="Contract address") + audit_links: Optional[List[str]] = Field(None, description="Audit links") + versionKey: Optional[str] = Field(None, description="Version key") + parentProtocol: Optional[str] = Field(None, description="Parent protocol") + previousNames: Optional[List[str]] = Field(None, description="Previous names") + latestFetchIsOk: bool = Field(..., description="Latest fetch status") + slug: str = Field(..., description="Protocol slug") + protocolType: str = Field(..., description="Protocol type") + total24h: Optional[float] = Field(None, description="24h total volume") + total48hto24h: Optional[float] = Field(None, description="48h to 24h total volume") + total7d: Optional[float] = Field(None, description="7d total volume") + totalAllTime: Optional[float] = Field(None, description="All time total volume") + totalDataChart: List = Field(default_factory=list, description="Total data chart") + totalDataChartBreakdown: List = Field(default_factory=list, description="Chart breakdown") + change_1d: Optional[float] = Field(None, description="1d change percentage") + error: Optional[str] = Field(None, description="Error message if any") + + +class DefiLlamaFetchDexSummary(DefiLlamaBaseTool): + """Tool for fetching DEX protocol summary data from DeFi Llama. + + This tool retrieves detailed information about a specific DEX protocol, + including metadata, metrics, and related protocols. + + Example: + summary_tool = DefiLlamaFetchDexSummary( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await summary_tool._arun(protocol="uniswap") + """ + + name: str = "defillama_fetch_dex_summary" + description: str = FETCH_DEX_SUMMARY_PROMPT + args_schema: Type[BaseModel] = FetchDexSummaryInput + + def _run( + self, protocol: str + ) -> FetchDexSummaryResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun( + self, protocol: str + ) -> FetchDexSummaryResponse: + """Fetch summary data for the given DEX protocol. + + Args: + protocol: Protocol identifier + + Returns: + FetchDexSummaryResponse containing protocol data or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchDexSummaryResponse(error=error_msg) + + # Fetch protocol data from API + result = await fetch_dex_summary(protocol=protocol) + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchDexSummaryResponse(error=result["error"]) + + # Return the response matching the API structure + return FetchDexSummaryResponse(**result) + + except Exception as e: + return FetchDexSummaryResponse(error=str(e)) diff --git a/skills/defillama/volumes/fetch_options_overview.py b/skills/defillama/volumes/fetch_options_overview.py new file mode 100644 index 0000000..57f27a8 --- /dev/null +++ b/skills/defillama/volumes/fetch_options_overview.py @@ -0,0 +1,107 @@ +"""Tool for fetching options overview data via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_options_overview + +FETCH_OPTIONS_OVERVIEW_PROMPT = """ +This tool fetches comprehensive overview data for all options protocols from DeFi Llama. +Returns detailed metrics including: +- Total volumes across different timeframes +- Change percentages +- Protocol-specific data +- Chain breakdowns +""" + +class ProtocolMethodology(BaseModel): + """Model representing protocol methodology data.""" + UserFees: Optional[str] = Field(None, description="User fees description") + Fees: Optional[str] = Field(None, description="Fees description") + Revenue: Optional[str] = Field(None, description="Revenue description") + ProtocolRevenue: Optional[str] = Field(None, description="Protocol revenue description") + HoldersRevenue: Optional[str] = Field(None, description="Holders revenue description") + SupplySideRevenue: Optional[str] = Field(None, description="Supply side revenue description") + +class Protocol(BaseModel): + """Model representing protocol data.""" + name: str = Field(..., description="Protocol name") + displayName: str = Field(..., description="Display name of protocol") + defillamaId: str = Field(..., description="DeFi Llama ID") + category: str = Field(..., description="Protocol category") + logo: str = Field(..., description="Logo URL") + chains: List[str] = Field(..., description="Supported chains") + module: str = Field(..., description="Protocol module") + total24h: Optional[float] = Field(None, description="24-hour total") + total7d: Optional[float] = Field(None, description="7-day total") + total30d: Optional[float] = Field(None, description="30-day total") + total1y: Optional[float] = Field(None, description="1-year total") + totalAllTime: Optional[float] = Field(None, description="All-time total") + change_1d: Optional[float] = Field(None, description="24-hour change percentage") + change_7d: Optional[float] = Field(None, description="7-day change percentage") + change_1m: Optional[float] = Field(None, description="30-day change percentage") + methodology: Optional[ProtocolMethodology] = Field(None, description="Protocol methodology") + breakdown24h: Optional[Dict[str, Dict[str, float]]] = Field(None, description="24-hour breakdown by chain") + breakdown30d: Optional[Dict[str, Dict[str, float]]] = Field(None, description="30-day breakdown by chain") + +class FetchOptionsOverviewResponse(BaseModel): + """Response schema for options overview data.""" + total24h: float = Field(..., description="Total volume in last 24 hours") + total7d: float = Field(..., description="Total volume in last 7 days") + total30d: float = Field(..., description="Total volume in last 30 days") + total1y: float = Field(..., description="Total volume in last year") + change_1d: float = Field(..., description="24-hour change percentage") + change_7d: float = Field(..., description="7-day change percentage") + change_1m: float = Field(..., description="30-day change percentage") + allChains: List[str] = Field(..., description="List of all chains") + protocols: List[Protocol] = Field(..., description="List of protocols") + error: Optional[str] = Field(None, description="Error message if any") + +class DefiLlamaFetchOptionsOverview(DefiLlamaBaseTool): + """Tool for fetching options overview data from DeFi Llama. + + This tool retrieves comprehensive data about all options protocols, + including volume metrics, change percentages, and detailed protocol information. + + Example: + overview_tool = DefiLlamaFetchOptionsOverview( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await overview_tool._arun() + """ + + name: str = "defillama_fetch_options_overview" + description: str = FETCH_OPTIONS_OVERVIEW_PROMPT + args_schema: Type[BaseModel] = BaseModel + + def _run(self) -> FetchOptionsOverviewResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self) -> FetchOptionsOverviewResponse: + """Fetch overview data for all options protocols. + + Returns: + FetchOptionsOverviewResponse containing comprehensive overview data or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchOptionsOverviewResponse(error=error_msg) + + # Fetch overview data from API + result = await fetch_options_overview() + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchOptionsOverviewResponse(error=result["error"]) + + # Return the parsed response + return FetchOptionsOverviewResponse(**result) + + except Exception as e: + return FetchOptionsOverviewResponse(error=str(e)) diff --git a/skills/defillama/yields/fetch_pool_chart.py b/skills/defillama/yields/fetch_pool_chart.py new file mode 100644 index 0000000..3f0b719 --- /dev/null +++ b/skills/defillama/yields/fetch_pool_chart.py @@ -0,0 +1,135 @@ +"""Tool for fetching pool chart data via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field +from datetime import datetime + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_pool_chart + +FETCH_POOL_CHART_PROMPT = """ +This tool fetches historical chart data from DeFi Llama for a specific pool. +Required: +- Pool ID +Returns historical data including: +- TVL in USD +- APY metrics (base, reward, total) +- Timestamps for each data point +""" + + +class PoolDataPoint(BaseModel): + """Model representing a single historical data point.""" + + timestamp: str = Field( + ..., + description="ISO formatted timestamp of the data point" + ) + tvlUsd: float = Field( + ..., + description="Total Value Locked in USD" + ) + apy: Optional[float] = Field( + None, + description="Total APY including rewards" + ) + apyBase: Optional[float] = Field( + None, + description="Base APY without rewards" + ) + apyReward: Optional[float] = Field( + None, + description="Additional APY from rewards" + ) + il7d: Optional[float] = Field( + None, + description="7-day impermanent loss" + ) + apyBase7d: Optional[float] = Field( + None, + description="7-day base APY" + ) + + +class FetchPoolChartInput(BaseModel): + """Input schema for fetching pool chart data.""" + + pool_id: str = Field( + ..., + description="ID of the pool to fetch chart data for" + ) + + +class FetchPoolChartResponse(BaseModel): + """Response schema for pool chart data.""" + + status: str = Field( + "success", + description="Response status" + ) + data: List[PoolDataPoint] = Field( + default_factory=list, + description="List of historical data points" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchPoolChart(DefiLlamaBaseTool): + """Tool for fetching pool chart data from DeFi Llama. + + This tool retrieves historical data for a specific pool, including + TVL and APY metrics over time. + + Example: + chart_tool = DefiLlamaFetchPoolChart( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await chart_tool._arun( + pool_id="747c1d2a-c668-4682-b9f9-296708a3dd90" + ) + """ + + name: str = "defillama_fetch_pool_chart" + description: str = FETCH_POOL_CHART_PROMPT + args_schema: Type[BaseModel] = FetchPoolChartInput + + def _run( + self, pool_id: str + ) -> FetchPoolChartResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun( + self, pool_id: str + ) -> FetchPoolChartResponse: + """Fetch historical chart data for the given pool. + + Args: + pool_id: ID of the pool to fetch chart data for + + Returns: + FetchPoolChartResponse containing historical data or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchPoolChartResponse(error=error_msg) + + # Fetch chart data from API + result = await fetch_pool_chart(pool_id=pool_id) + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchPoolChartResponse(error=result["error"]) + + # Return the response matching the API structure + return FetchPoolChartResponse(**result) + + except Exception as e: + return FetchPoolChartResponse(error=str(e)) diff --git a/skills/defillama/yields/fetch_pools.py b/skills/defillama/yields/fetch_pools.py new file mode 100644 index 0000000..a2748a7 --- /dev/null +++ b/skills/defillama/yields/fetch_pools.py @@ -0,0 +1,217 @@ +"""Tool for fetching pool data via DeFi Llama API.""" + +from typing import Dict, List, Optional, Type +from pydantic import BaseModel, Field + +from skills.defillama.base import DefiLlamaBaseTool +from skills.defillama.api import fetch_pools + +FETCH_POOLS_PROMPT = """ +This tool fetches comprehensive data about yield-generating pools from DeFi Llama. +Returns data including: +- Pool details (chain, project, symbol) +- TVL and APY information +- Statistical metrics (mean, standard deviation) +- Risk assessments and predictions +- Historical performance data +""" + + +class PredictionData(BaseModel): + """Model representing prediction data for a pool.""" + + predictedClass: Optional[str] = Field( + None, + description="Predicted direction of APY movement" + ) + predictedProbability: Optional[float] = Field( + None, + description="Probability of the prediction" + ) + binnedConfidence: Optional[int] = Field( + None, + description="Confidence level bucket" + ) + + +class PoolData(BaseModel): + """Model representing a single pool's data.""" + + chain: str = Field( + ..., + description="Blockchain network" + ) + project: str = Field( + ..., + description="Protocol or project name" + ) + symbol: str = Field( + ..., + description="Token or pool symbol" + ) + tvlUsd: float = Field( + ..., + description="Total Value Locked in USD" + ) + apyBase: Optional[float] = Field( + None, + description="Base APY without rewards" + ) + apyReward: Optional[float] = Field( + None, + description="Additional APY from rewards" + ) + apy: Optional[float] = Field( + None, + description="Total APY including rewards" + ) + rewardTokens: Optional[List[str]] = Field( + None, + description="List of reward token addresses" + ) + pool: Optional[str] = Field( + None, + description="Pool identifier" + ) + apyPct1D: Optional[float] = Field( + None, + description="1-day APY percentage change" + ) + apyPct7D: Optional[float] = Field( + None, + description="7-day APY percentage change" + ) + apyPct30D: Optional[float] = Field( + None, + description="30-day APY percentage change" + ) + stablecoin: bool = Field( + False, + description="Whether pool involves stablecoins" + ) + ilRisk: str = Field( + "no", + description="Impermanent loss risk assessment" + ) + exposure: str = Field( + "single", + description="Asset exposure type" + ) + predictions: Optional[PredictionData] = Field( + None, + description="APY movement predictions" + ) + poolMeta: Optional[str] = Field( + None, + description="Additional pool metadata" + ) + mu: Optional[float] = Field( + None, + description="Mean APY value" + ) + sigma: Optional[float] = Field( + None, + description="APY standard deviation" + ) + count: Optional[int] = Field( + None, + description="Number of data points" + ) + outlier: bool = Field( + False, + description="Whether pool is an outlier" + ) + underlyingTokens: Optional[List[str]] = Field( + None, + description="List of underlying token addresses" + ) + il7d: Optional[float] = Field( + None, + description="7-day impermanent loss" + ) + apyBase7d: Optional[float] = Field( + None, + description="7-day base APY" + ) + apyMean30d: Optional[float] = Field( + None, + description="30-day mean APY" + ) + volumeUsd1d: Optional[float] = Field( + None, + description="24h volume in USD" + ) + volumeUsd7d: Optional[float] = Field( + None, + description="7-day volume in USD" + ) + apyBaseInception: Optional[float] = Field( + None, + description="Base APY since inception" + ) + + +class FetchPoolsResponse(BaseModel): + """Response schema for pool data.""" + + status: str = Field( + "success", + description="Response status" + ) + data: List[PoolData] = Field( + default_factory=list, + description="List of pool data" + ) + error: Optional[str] = Field( + None, + description="Error message if any" + ) + + +class DefiLlamaFetchPools(DefiLlamaBaseTool): + """Tool for fetching pool data from DeFi Llama. + + This tool retrieves comprehensive data about yield-generating pools, + including TVL, APYs, risk metrics, and predictions. + + Example: + pools_tool = DefiLlamaFetchPools( + skill_store=store, + agent_id="agent_123", + agent_store=agent_store + ) + result = await pools_tool._arun() + """ + + name: str = "defillama_fetch_pools" + description: str = FETCH_POOLS_PROMPT + args_schema: None = None # No input parameters needed + + def _run(self) -> FetchPoolsResponse: + """Synchronous implementation - not supported.""" + raise NotImplementedError("Use _arun instead") + + async def _arun(self) -> FetchPoolsResponse: + """Fetch pool data. + + Returns: + FetchPoolsResponse containing pool data or error + """ + try: + # Check rate limiting + is_rate_limited, error_msg = await self.check_rate_limit() + if is_rate_limited: + return FetchPoolsResponse(error=error_msg) + + # Fetch pool data from API + result = await fetch_pools() + + # Check for API errors + if isinstance(result, dict) and "error" in result: + return FetchPoolsResponse(error=result["error"]) + + # Return the response matching the API structure + return FetchPoolsResponse(**result) + + except Exception as e: + return FetchPoolsResponse(error=str(e)) From 934a873d7cbbcc9c99e69a18bb8d17a9507c0fd7 Mon Sep 17 00:00:00 2001 From: Kiearn Williams Date: Sun, 23 Feb 2025 13:12:28 +0000 Subject: [PATCH 3/3] fix: increase response timeout value --- skills/defillama/tests/api_integration.test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/defillama/tests/api_integration.test.py b/skills/defillama/tests/api_integration.test.py index 0911d83..1a098c1 100644 --- a/skills/defillama/tests/api_integration.test.py +++ b/skills/defillama/tests/api_integration.test.py @@ -65,7 +65,7 @@ def setUp(self): """Set up the async event loop""" self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) - self.timeout = 30 + self.timeout = 3000 def tearDown(self): """Clean up the event loop"""