Coverage for src/stable_yield_lab/sources/beefy.py: 80%
47 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-04 20:38 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-04 20:38 +0000
1"""Beefy Finance adapter for :mod:`stable_yield_lab`."""
3from __future__ import annotations
5import json
6import logging
7import urllib.request
8from datetime import UTC, datetime
9from pathlib import Path
10from typing import Any
12from ..core import Pool, STABLE_TOKENS
14logger = logging.getLogger(__name__)
17class BeefySource:
18 """HTTP client for Beefy vault data."""
20 VAULTS_URL = "https://api.beefy.finance/vaults"
21 APY_URL = "https://api.beefy.finance/apy"
22 TVL_URL = "https://api.beefy.finance/tvl"
24 CHAIN_IDS = {
25 "ethereum": "1",
26 "bsc": "56",
27 "polygon": "137",
28 "arbitrum": "42161",
29 "optimism": "10",
30 }
32 def __init__(self, cache_dir: str | None = None) -> None:
33 self.cache_dir = Path(cache_dir) if cache_dir else None
35 def _get_json(self, name: str, url: str) -> Any:
36 if self.cache_dir:
37 path = self.cache_dir / f"{name}.json"
38 if path.exists():
39 with path.open() as f:
40 return json.load(f)
41 with urllib.request.urlopen(url) as resp: # pragma: no cover - network path
42 data = json.load(resp)
43 if self.cache_dir:
44 self.cache_dir.mkdir(parents=True, exist_ok=True)
45 with (self.cache_dir / f"{name}.json").open("w") as f:
46 json.dump(data, f)
47 return data
49 def fetch(self) -> list[Pool]:
50 try:
51 vaults = self._get_json("vaults", self.VAULTS_URL)
52 apy = self._get_json("apy", self.APY_URL)
53 tvl = self._get_json("tvl", self.TVL_URL)
54 except Exception as exc: # pragma: no cover - network errors
55 logger.warning("Beefy request failed: %s", exc)
56 return []
57 pools: list[Pool] = []
58 now = datetime.now(tz=UTC).timestamp()
59 for v in vaults:
60 if v.get("status") != "active": 60 ↛ 61line 60 didn't jump to line 61 because the condition on line 60 was never true
61 continue
62 assets = v.get("assets") or []
63 if not assets or not all( 63 ↛ 66line 63 didn't jump to line 66 because the condition on line 63 was never true
64 a.upper() in STABLE_TOKENS or "USD" in a.upper() for a in assets
65 ):
66 continue
67 chain = str(v.get("chain", ""))
68 chain_id = self.CHAIN_IDS.get(chain.lower(), chain)
69 tvl_usd = float(tvl.get(str(chain_id), {}).get(v["id"], 0.0))
70 base = float(apy.get(v["id"], 0.0))
71 pools.append(
72 Pool(
73 name=str(v.get("name", v["id"])),
74 chain=chain,
75 stablecoin=str(assets[0]),
76 tvl_usd=tvl_usd,
77 base_apy=base,
78 reward_apy=0.0,
79 is_auto=True,
80 source="beefy",
81 timestamp=now,
82 )
83 )
84 return pools
87__all__ = ["BeefySource"]