Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 32 additions & 33 deletions hyperliquid/api.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,41 @@
import json
import logging
from json import JSONDecodeError

import requests
from typing import Any, Dict, Optional

from hyperliquid.utils.constants import MAINNET_API_URL
from hyperliquid.utils.error import ClientError, ServerError
from hyperliquid.utils.types import Any

from .utils.error_handling import APIError, retry_on_failure

class API:
def __init__(self, base_url=None):
self.base_url = base_url or MAINNET_API_URL
def __init__(self, base_url: Optional[str] = None):
self.base_url = base_url or "https://api.hyperliquid.xyz"
self.session = requests.Session()
self.session.headers.update({"Content-Type": "application/json"})
self._logger = logging.getLogger(__name__)

def post(self, url_path: str, payload: Any = None) -> Any:
payload = payload or {}
url = self.base_url + url_path
response = self.session.post(url, json=payload)
self._handle_exception(response)
@retry_on_failure(max_retries=3)
def post(self, endpoint: str, payload: Dict[str, Any]) -> Any:
"""Make a POST request to the API with retry mechanism.

Args:
endpoint: API endpoint
payload: Request payload

Returns:
API response

Raises:
APIError: If the request fails
"""
try:
response = self.session.post(
f"{self.base_url}{endpoint}",
json=payload,
headers={"Content-Type": "application/json"}
)
response.raise_for_status()
return response.json()
except ValueError:
return {"error": f"Could not parse JSON: {response.text}"}

def _handle_exception(self, response):
status_code = response.status_code
if status_code < 400:
return
if 400 <= status_code < 500:
try:
err = json.loads(response.text)
except JSONDecodeError:
raise ClientError(status_code, None, response.text, None, response.headers)
if err is None:
raise ClientError(status_code, None, response.text, None, response.headers)
error_data = err.get("data")
raise ClientError(status_code, err["code"], err["msg"], response.headers, error_data)
raise ServerError(status_code, response.text)
except requests.exceptions.RequestException as e:
raise APIError(
f"API request failed: {str(e)}",
status_code=getattr(e.response, 'status_code', None),
response=getattr(e.response, 'text', None)
)
except json.JSONDecodeError as e:
raise APIError(f"Failed to decode API response: {str(e)}")
17 changes: 17 additions & 0 deletions hyperliquid/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from .error_handling import (
HyperliquidError,
InvalidParameterError,
APIError,
validate_address,
retry_on_failure,
validate_numeric_param
)

__all__ = [
'HyperliquidError',
'InvalidParameterError',
'APIError',
'validate_address',
'retry_on_failure',
'validate_numeric_param'
]
126 changes: 126 additions & 0 deletions hyperliquid/utils/error_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from typing import Any, Callable, Optional, TypeVar, Union
import time
from functools import wraps

T = TypeVar('T')

class HyperliquidError(Exception):
"""Base exception class for Hyperliquid SDK."""
pass

class InvalidParameterError(HyperliquidError):
"""Raised when an invalid parameter is provided."""
pass

class APIError(HyperliquidError):
"""Raised when an API call fails."""
def __init__(self, message: str, status_code: Optional[int] = None, response: Any = None):
super().__init__(message)
self.status_code = status_code
self.response = response

def validate_address(address: str) -> bool:
"""Validate an Ethereum address.

Args:
address (str): The address to validate.

Returns:
bool: True if the address is valid, False otherwise.
"""
if not isinstance(address, str):
return False
if not address.startswith('0x'):
return False
if len(address) != 42:
return False
try:
int(address[2:], 16)
return True
except ValueError:
return False

def retry_on_failure(
max_retries: int = 3,
initial_delay: float = 1.0,
max_delay: float = 10.0,
backoff_factor: float = 2.0,
exceptions: tuple = (APIError,)
) -> Callable:
"""Decorator to retry functions on failure with exponential backoff.

Args:
max_retries (int): Maximum number of retries before giving up.
initial_delay (float): Initial delay between retries in seconds.
max_delay (float): Maximum delay between retries in seconds.
backoff_factor (float): Factor to multiply delay by after each retry.
exceptions (tuple): Tuple of exceptions to retry on.

Returns:
Callable: Decorated function that will retry on failure.
"""
def decorator(func: Callable[..., T]) -> Callable[..., T]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> T:
delay = initial_delay
last_exception = None

for retry in range(max_retries + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if retry == max_retries:
break

time.sleep(delay)
delay = min(delay * backoff_factor, max_delay)

raise last_exception # type: ignore

return wrapper
return decorator

def validate_numeric_param(
value: Union[int, float, str],
param_name: str,
min_value: Optional[Union[int, float]] = None,
max_value: Optional[Union[int, float]] = None
) -> Union[int, float]:
"""Validate a numeric parameter.

Args:
value: The value to validate.
param_name: Name of the parameter (for error messages).
min_value: Minimum allowed value (inclusive).
max_value: Maximum allowed value (inclusive).

Returns:
Union[int, float]: The validated numeric value.

Raises:
InvalidParameterError: If validation fails.
"""
try:
if isinstance(value, str):
# Try to convert string to float/int
if '.' in value:
num_value = float(value)
else:
num_value = int(value)
else:
num_value = value
except (ValueError, TypeError):
raise InvalidParameterError(f"Invalid {param_name}: must be a valid number")

if min_value is not None and num_value < min_value:
raise InvalidParameterError(
f"Invalid {param_name}: {num_value} is less than minimum allowed value of {min_value}"
)

if max_value is not None and num_value > max_value:
raise InvalidParameterError(
f"Invalid {param_name}: {num_value} is greater than maximum allowed value of {max_value}"
)

return num_value