7 new endpoints under /api/v1/defi/ (all free, no API key):
- GET /defi/tvl/protocols - top DeFi protocols by TVL
- GET /defi/tvl/chains - chain TVL rankings
- GET /defi/tvl/{protocol} - single protocol TVL
- GET /defi/yields - top yield pools (filter by chain/project)
- GET /defi/stablecoins - stablecoin market data
- GET /defi/volumes/dexs - DEX volume overview
- GET /defi/fees - protocol fees/revenue overview
Data source: DefiLlama API (free, no key needed)
58 new tests (33 service + 25 route). All 561 tests passing.
196 lines
6.6 KiB
Python
196 lines
6.6 KiB
Python
"""DeFi data service via DefiLlama API (no API key required)."""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
LLAMA_BASE = "https://api.llama.fi"
|
|
STABLES_BASE = "https://stablecoins.llama.fi"
|
|
YIELDS_BASE = "https://yields.llama.fi"
|
|
TIMEOUT = 15.0
|
|
|
|
|
|
async def get_top_protocols(limit: int = 20) -> list[dict[str, Any]]:
|
|
"""Fetch top DeFi protocols ranked by TVL from DefiLlama."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
|
|
resp = await client.get(f"{LLAMA_BASE}/protocols")
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
return [
|
|
{
|
|
"name": p.get("name"),
|
|
"symbol": p.get("symbol"),
|
|
"tvl": p.get("tvl"),
|
|
"chain": p.get("chain"),
|
|
"chains": p.get("chains", []),
|
|
"category": p.get("category"),
|
|
"change_1d": p.get("change_1d"),
|
|
"change_7d": p.get("change_7d"),
|
|
}
|
|
for p in data[:limit]
|
|
]
|
|
except Exception:
|
|
logger.exception("Failed to fetch top protocols from DefiLlama")
|
|
return []
|
|
|
|
|
|
async def get_chain_tvls() -> list[dict[str, Any]]:
|
|
"""Fetch TVL rankings for all chains from DefiLlama."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
|
|
resp = await client.get(f"{LLAMA_BASE}/v2/chains")
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
return [
|
|
{
|
|
"name": c.get("name"),
|
|
"tvl": c.get("tvl"),
|
|
"tokenSymbol": c.get("tokenSymbol"),
|
|
}
|
|
for c in data
|
|
]
|
|
except Exception:
|
|
logger.exception("Failed to fetch chain TVLs from DefiLlama")
|
|
return []
|
|
|
|
|
|
async def get_protocol_tvl(protocol: str) -> float | None:
|
|
"""Fetch current TVL for a specific protocol slug."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
|
|
resp = await client.get(f"{LLAMA_BASE}/tvl/{protocol}")
|
|
resp.raise_for_status()
|
|
return resp.json()
|
|
except Exception:
|
|
logger.exception("Failed to fetch TVL for protocol %s", protocol)
|
|
return None
|
|
|
|
|
|
async def get_yield_pools(
|
|
chain: str | None = None,
|
|
project: str | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
"""Fetch yield pools from DefiLlama, optionally filtered by chain and/or project.
|
|
|
|
Returns top 20 by TVL descending.
|
|
"""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
|
|
resp = await client.get(f"{YIELDS_BASE}/pools")
|
|
resp.raise_for_status()
|
|
payload = resp.json()
|
|
pools: list[dict[str, Any]] = payload.get("data", [])
|
|
|
|
if chain is not None:
|
|
pools = [p for p in pools if p.get("chain") == chain]
|
|
if project is not None:
|
|
pools = [p for p in pools if p.get("project") == project]
|
|
|
|
pools = sorted(pools, key=lambda p: p.get("tvlUsd") or 0, reverse=True)[:20]
|
|
|
|
return [
|
|
{
|
|
"pool": p.get("pool"),
|
|
"chain": p.get("chain"),
|
|
"project": p.get("project"),
|
|
"symbol": p.get("symbol"),
|
|
"tvlUsd": p.get("tvlUsd"),
|
|
"apy": p.get("apy"),
|
|
"apyBase": p.get("apyBase"),
|
|
"apyReward": p.get("apyReward"),
|
|
}
|
|
for p in pools
|
|
]
|
|
except Exception:
|
|
logger.exception("Failed to fetch yield pools from DefiLlama")
|
|
return []
|
|
|
|
|
|
def _extract_circulating(asset: dict[str, Any]) -> float | None:
|
|
"""Extract the primary circulating supply value from a stablecoin asset dict."""
|
|
raw = asset.get("circulating")
|
|
if raw is None:
|
|
return None
|
|
if isinstance(raw, (int, float)):
|
|
return float(raw)
|
|
if isinstance(raw, dict):
|
|
# DefiLlama returns {"peggedUSD": <amount>, ...}
|
|
values = [v for v in raw.values() if isinstance(v, (int, float))]
|
|
return values[0] if values else None
|
|
return None
|
|
|
|
|
|
async def get_stablecoins(limit: int = 20) -> list[dict[str, Any]]:
|
|
"""Fetch top stablecoins by circulating supply from DefiLlama."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
|
|
resp = await client.get(f"{STABLES_BASE}/stablecoins")
|
|
resp.raise_for_status()
|
|
payload = resp.json()
|
|
assets: list[dict[str, Any]] = payload.get("peggedAssets", [])
|
|
|
|
return [
|
|
{
|
|
"name": a.get("name"),
|
|
"symbol": a.get("symbol"),
|
|
"pegType": a.get("pegType"),
|
|
"circulating": _extract_circulating(a),
|
|
"price": a.get("price"),
|
|
}
|
|
for a in assets[:limit]
|
|
]
|
|
except Exception:
|
|
logger.exception("Failed to fetch stablecoins from DefiLlama")
|
|
return []
|
|
|
|
|
|
async def get_dex_volumes() -> dict[str, Any] | None:
|
|
"""Fetch DEX volume overview from DefiLlama."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
|
|
resp = await client.get(f"{LLAMA_BASE}/overview/dexs")
|
|
resp.raise_for_status()
|
|
payload = resp.json()
|
|
|
|
protocols = [
|
|
{
|
|
"name": p.get("name"),
|
|
"volume24h": p.get("total24h"),
|
|
}
|
|
for p in payload.get("protocols", [])
|
|
]
|
|
|
|
return {
|
|
"totalVolume24h": payload.get("total24h"),
|
|
"totalVolume7d": payload.get("total7d"),
|
|
"protocols": protocols,
|
|
}
|
|
except Exception:
|
|
logger.exception("Failed to fetch DEX volumes from DefiLlama")
|
|
return None
|
|
|
|
|
|
async def get_protocol_fees() -> list[dict[str, Any]]:
|
|
"""Fetch protocol fees and revenue overview from DefiLlama."""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=TIMEOUT) as client:
|
|
resp = await client.get(f"{LLAMA_BASE}/overview/fees")
|
|
resp.raise_for_status()
|
|
payload = resp.json()
|
|
|
|
return [
|
|
{
|
|
"name": p.get("name"),
|
|
"fees24h": p.get("total24h"),
|
|
"revenue24h": p.get("revenue24h"),
|
|
}
|
|
for p in payload.get("protocols", [])
|
|
]
|
|
except Exception:
|
|
logger.exception("Failed to fetch protocol fees from DefiLlama")
|
|
return []
|