diff --git a/src/cerbero_mcp/exchanges/deribit/client.py b/src/cerbero_mcp/exchanges/deribit/client.py index e779b3d..b4629c8 100644 --- a/src/cerbero_mcp/exchanges/deribit/client.py +++ b/src/cerbero_mcp/exchanges/deribit/client.py @@ -23,6 +23,10 @@ RESOLUTION_MAP = { } +class DeribitAuthError(Exception): + """Deribit auth failed (bad credentials, missing scope, env mismatch).""" + + @dataclass class DeribitClient: client_id: str @@ -50,6 +54,13 @@ class DeribitClient: async with async_client(timeout=15.0) as http: resp = await http.get(url, params=params) data = resp.json() + if "result" not in data: + error = data.get("error", {}) + msg = error.get("message", str(data)) if isinstance(error, dict) else str(error) + code = error.get("code") if isinstance(error, dict) else None + raise DeribitAuthError( + f"Deribit auth failed (code={code}, env={'testnet' if self.testnet else 'mainnet'}): {msg}" + ) result = data["result"] self._token = result["access_token"] self._token_expires_at = time.monotonic() + result.get("expires_in", 900) - 30 @@ -63,7 +74,10 @@ class DeribitClient: async def _request(self, method: str, params: dict[str, Any] | None = None) -> dict: is_private = method.startswith("private/") if is_private: - await self._get_token() + try: + await self._get_token() + except DeribitAuthError as e: + return {"result": None, "error": str(e)} url = f"{self.base_url}/{method}" request_params = dict(params) if params else {} diff --git a/tests/unit/exchanges/deribit/test_client.py b/tests/unit/exchanges/deribit/test_client.py index e46b20a..f7bc1df 100644 --- a/tests/unit/exchanges/deribit/test_client.py +++ b/tests/unit/exchanges/deribit/test_client.py @@ -154,6 +154,23 @@ async def test_get_account_summary(httpx_mock: HTTPXMock, client: DeribitClient) assert result["balance"] == 900.0 +@pytest.mark.asyncio +async def test_private_call_with_bad_auth_returns_error_envelope( + httpx_mock: HTTPXMock, client: DeribitClient +): + """Auth fallita (creds errate / scope mancante) → error envelope, non KeyError.""" + httpx_mock.add_response( + url=re.compile(r"https://test\.deribit\.com/api/v2/public/auth"), + json={"error": {"code": 13004, "message": "invalid_credentials"}}, + is_reusable=True, + ) + summary = await client.get_account_summary("USDC") + assert summary["equity"] == 0 + assert "invalid_credentials" in summary["error"] + positions = await client.get_positions("USDC") + assert positions == [] + + @pytest.mark.asyncio async def test_place_order(httpx_mock: HTTPXMock, client: DeribitClient): httpx_mock.add_response(