Skip to content

Commit f7573f0

Browse files
committed
feat(hyperliquid): add methods for info endpoint
1 parent bb76bfc commit f7573f0

File tree

8 files changed

+308
-99
lines changed

8 files changed

+308
-99
lines changed

api/read/hyperliquid.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
HTTPGetLogsLatencyMetric,
1010
HTTPTxReceiptLatencyMetric,
1111
)
12+
from metrics.hyperliquid_info import (
13+
HTTPClearinghouseStateLatencyMetric,
14+
HTTPOpenOrdersLatencyMetric,
15+
)
1216

1317
METRIC_NAME = f"{MetricsServiceConfig.METRIC_PREFIX}response_latency_seconds"
1418
ALLOWED_REGIONS: list[str] = [
@@ -26,6 +30,8 @@
2630
(HTTPAccBalanceLatencyMetric, METRIC_NAME),
2731
(HTTPTxReceiptLatencyMetric, METRIC_NAME),
2832
(HTTPGetLogsLatencyMetric, METRIC_NAME),
33+
(HTTPClearinghouseStateLatencyMetric, METRIC_NAME),
34+
(HTTPOpenOrdersLatencyMetric, METRIC_NAME),
2935
]
3036
if os.getenv("VERCEL_REGION") in ALLOWED_REGIONS # System env var, standard name
3137
else []

common/http_timing.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""Shared HTTP request timing utilities for metric collection."""
2+
3+
import asyncio
4+
import time
5+
from typing import Any, Dict, Optional
6+
7+
import aiohttp
8+
9+
MAX_RETRIES = 2
10+
11+
12+
class HttpTimingCollector:
13+
"""Utility class for measuring HTTP request timing with detailed breakdown."""
14+
15+
def __init__(self):
16+
self.timing: Dict[str, float] = {}
17+
18+
def create_trace_config(self) -> aiohttp.TraceConfig:
19+
"""Create aiohttp trace configuration for detailed timing measurement."""
20+
trace_config = aiohttp.TraceConfig()
21+
22+
async def on_request_start(session, context, params):
23+
self.timing["start"] = time.monotonic()
24+
25+
async def on_dns_resolvehost_start(session, context, params):
26+
self.timing["dns_start"] = time.monotonic()
27+
28+
async def on_dns_resolvehost_end(session, context, params):
29+
self.timing["dns_end"] = time.monotonic()
30+
31+
async def on_connection_create_start(session, context, params):
32+
self.timing["conn_start"] = time.monotonic()
33+
34+
async def on_connection_create_end(session, context, params):
35+
self.timing["conn_end"] = time.monotonic()
36+
37+
async def on_request_end(session, context, params):
38+
self.timing["end"] = time.monotonic()
39+
40+
trace_config.on_request_start.append(on_request_start)
41+
trace_config.on_dns_resolvehost_start.append(on_dns_resolvehost_start)
42+
trace_config.on_dns_resolvehost_end.append(on_dns_resolvehost_end)
43+
trace_config.on_connection_create_start.append(on_connection_create_start)
44+
trace_config.on_connection_create_end.append(on_connection_create_end)
45+
trace_config.on_request_end.append(on_request_end)
46+
47+
return trace_config
48+
49+
def get_connection_time(self) -> float:
50+
"""Get connection establishment time in seconds."""
51+
if "conn_start" in self.timing and "conn_end" in self.timing:
52+
return self.timing["conn_end"] - self.timing["conn_start"]
53+
return 0.0
54+
55+
def get_dns_time(self) -> float:
56+
"""Get DNS resolution time in seconds."""
57+
if "dns_start" in self.timing and "dns_end" in self.timing:
58+
return self.timing["dns_end"] - self.timing["dns_start"]
59+
return 0.0
60+
61+
62+
async def measure_http_request_timing(
63+
session: aiohttp.ClientSession,
64+
method: str,
65+
url: str,
66+
headers: Optional[Dict[str, str]] = None,
67+
json_data: Optional[Dict[str, Any]] = None,
68+
exclude_connection_time: bool = True,
69+
) -> tuple[float, aiohttp.ClientResponse]:
70+
"""Measure HTTP request timing with retry logic and detailed breakdown.
71+
72+
Returns:
73+
tuple: (response_time_seconds, response)
74+
"""
75+
response_time = 0.0
76+
response = None
77+
78+
for retry_count in range(MAX_RETRIES):
79+
start_time = time.monotonic()
80+
81+
# Send request
82+
if method.upper() == "POST":
83+
response = await session.post(
84+
url, headers=headers, json=json_data
85+
)
86+
else:
87+
response = await session.get(url, headers=headers)
88+
89+
response_time = time.monotonic() - start_time
90+
91+
# Handle rate limiting
92+
if response.status == 429 and retry_count < MAX_RETRIES - 1:
93+
wait_time = int(response.headers.get("Retry-After", 3))
94+
await response.release()
95+
await asyncio.sleep(wait_time)
96+
continue
97+
98+
break
99+
100+
if not response:
101+
raise ValueError("No response received")
102+
103+
return response_time, response

common/hyperliquid_info_base.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Base class for Hyperliquid /info endpoint metrics."""
2+
3+
from abc import abstractmethod
4+
from typing import Any
5+
6+
import aiohttp
7+
8+
from common.http_timing import measure_http_request_timing
9+
from common.metric_config import MetricConfig, MetricLabelKey, MetricLabels
10+
from common.metric_types import HttpMetric
11+
from common.metrics_handler import MetricsHandler
12+
13+
14+
class HyperliquidInfoMetricBase(HttpMetric):
15+
"""Base class for Hyperliquid /info endpoint latency metrics.
16+
17+
Handles request configuration, state validation, and response time
18+
measurement for Hyperliquid info API endpoints.
19+
"""
20+
21+
@property
22+
@abstractmethod
23+
def method(self) -> str:
24+
"""Info API method to be implemented by subclasses."""
25+
pass
26+
27+
def __init__(
28+
self,
29+
handler: "MetricsHandler",
30+
metric_name: str,
31+
labels: MetricLabels,
32+
config: MetricConfig,
33+
**kwargs: Any,
34+
) -> None:
35+
"""Initialize Hyperliquid info metric with state-based parameters."""
36+
state_data = kwargs.get("state_data", {})
37+
if not self.validate_state(state_data):
38+
raise ValueError(f"Invalid state data for {self.method}")
39+
40+
super().__init__(
41+
handler=handler,
42+
metric_name=metric_name,
43+
labels=labels,
44+
config=config,
45+
)
46+
47+
params = self.get_params_from_state(state_data)
48+
self.user_address = params["user"]
49+
self.labels.update_label(MetricLabelKey.API_METHOD, self.method)
50+
self.request_payload = self._build_request_payload()
51+
52+
def _build_request_payload(self) -> dict[str, Any]:
53+
"""Build the Hyperliquid info API request payload."""
54+
return {"type": self.method, "user": self.user_address}
55+
56+
@staticmethod
57+
def validate_state(state_data: dict[str, Any]) -> bool:
58+
"""Validate state data. Override in subclasses if needed."""
59+
return True
60+
61+
@staticmethod
62+
def get_params_from_state(state_data: dict[str, Any]) -> dict[str, str]:
63+
"""Get parameters from state data. Override in subclasses if needed."""
64+
return {}
65+
66+
def get_info_endpoint(self) -> str:
67+
"""Transform EVM endpoint to info endpoint."""
68+
base_endpoint = self.get_endpoint()
69+
70+
if base_endpoint.endswith("/evm"):
71+
return base_endpoint.replace("/evm", "/info")
72+
else:
73+
# Handle cases where endpoint doesn't end with /evm
74+
if base_endpoint.endswith("/"):
75+
return base_endpoint + "info"
76+
else:
77+
return base_endpoint + "/info"
78+
79+
async def fetch_data(self) -> float:
80+
"""Measure single request latency for Hyperliquid info API."""
81+
endpoint = self.get_info_endpoint()
82+
83+
headers = {
84+
"Accept": "application/json",
85+
"Content-Type": "application/json",
86+
}
87+
88+
async with aiohttp.ClientSession() as session:
89+
response_time, response = await measure_http_request_timing(
90+
session=session,
91+
method="POST",
92+
url=endpoint,
93+
headers=headers,
94+
json_data=self.request_payload,
95+
exclude_connection_time=True,
96+
)
97+
98+
try:
99+
# Validate response
100+
if response.status != 200:
101+
raise aiohttp.ClientResponseError(
102+
request_info=response.request_info,
103+
history=(),
104+
status=response.status,
105+
message=f"Status code: {response.status}",
106+
headers=response.headers,
107+
)
108+
109+
response_data = await response.json()
110+
111+
return response_time
112+
113+
finally:
114+
await response.release()
115+
116+
def process_data(self, value: float) -> float:
117+
"""Process raw latency measurement."""
118+
return value

0 commit comments

Comments
 (0)