11import asyncio
2+ from enum import StrEnum
23import time
34
45from loguru import logger
1617from pusher .price_state import PriceState
1718
1819
20+ class PushErrorReason (StrEnum ):
21+ """ setOracle push failure modes """
22+ # 2.5s rate limit reject, expected with redundant relayers
23+ RATE_LIMIT = "rate_limit"
24+ # Per-account limit, need to purchase more transactions with reserveRequestWeight
25+ USER_LIMIT = "user_limit"
26+ # Some exception thrown internally
27+ INTERNAL_ERROR = "internal_error"
28+ # Invalid nonce, if the pusher account pushes multiple transactions with the same ms timestamp
29+ INVALID_NONCE = "invalid_nonce"
30+ # Some error string we haven't categorized yet
31+ UNKNOWN = "unknown"
32+
33+
1934class Publisher :
2035 """
2136 HIP-3 oracle publisher handler
@@ -51,6 +66,8 @@ def __init__(self, config: Config, price_state: PriceState, metrics: Metrics):
5166 if not config .multisig .multisig_address :
5267 raise Exception ("Multisig enabled but missing multisig address" )
5368 self .multisig_address = config .multisig .multisig_address
69+ else :
70+ self .multisig_address = None
5471
5572 self .market_name = config .hyperliquid .market_name
5673 self .enable_publish = config .hyperliquid .enable_publish
@@ -79,9 +96,6 @@ def publish(self):
7996 # markPxs is a list of dicts of length 0-2, and so can be empty
8097 mark_pxs = [mark_pxs ] if mark_pxs else []
8198
82- # TODO: "Each update can change oraclePx and markPx by at most 1%."
83- # TODO: "The markPx cannot be updated such that open interest would be 10x the open interest cap."
84-
8599 if self .enable_publish :
86100 try :
87101 if self .enable_kms :
@@ -103,12 +117,13 @@ def publish(self):
103117 all_mark_pxs = mark_pxs ,
104118 external_perp_pxs = external_perp_pxs ,
105119 )
106- self ._handle_response (push_response )
120+ self ._handle_response (push_response , list ( oracle_pxs . keys ()) )
107121 except PushError :
108- logger . error ( "All push attempts failed" )
109- self . metrics . failed_push_counter . add ( 1 , self . metrics_labels )
122+ # since rate limiting is expected, don't necessarily log
123+ pass
110124 except Exception as e :
111125 logger .exception ("Unexpected exception in push request: {}" , repr (e ))
126+ self ._update_attempts_total ("error" , PushErrorReason .INTERNAL_ERROR , list (oracle_pxs .keys ()))
112127 else :
113128 logger .debug ("push disabled" )
114129
@@ -128,14 +143,27 @@ def _send_update(self, oracle_pxs, all_mark_pxs, external_perp_pxs):
128143
129144 raise PushError ("all push endpoints failed" )
130145
131- def _handle_response (self , response ):
146+ def _handle_response (self , response , symbols : list [ str ] ):
132147 logger .debug ("oracle update response: {}" , response )
133148 status = response .get ("status" )
134149 if status == "ok" :
135- self .metrics .successful_push_counter .add (1 , self .metrics_labels )
150+ self ._update_attempts_total ("success" , None , symbols )
151+ time_secs = int (time .time ())
152+
153+ # update last publish time for each symbol in dex
154+ for symbol in symbols :
155+ labels = {** self .metrics_labels , "symbol" : symbol }
156+ self .metrics .last_pushed_time .set (time_secs , labels )
157+
158+ # log any data in the ok response (likely price clamping issues)
159+ ok_data = response .get ("response" , {}).get ("data" )
160+ if ok_data :
161+ logger .info ("ok response data: {}" , ok_data )
136162 elif status == "err" :
137- self .metrics .failed_push_counter .add (1 , self .metrics_labels )
138- logger .error ("oracle update error response: {}" , response )
163+ error_reason = self ._get_error_reason (response )
164+ self ._update_attempts_total ("error" , error_reason , symbols )
165+ if error_reason != "rate_limit" :
166+ logger .error ("Error response: {}" , response )
139167
140168 def _record_push_interval_metric (self ):
141169 now = time .time ()
@@ -183,3 +211,29 @@ def _send_single_multisig_update(self, exchange, oracle_pxs, all_mark_pxs, exter
183211 outer_signer = self .oracle_account .address ,
184212 )]
185213 return exchange .multi_sig (self .multisig_address , action , signatures , timestamp )
214+
215+ def _update_attempts_total (self , status : str , error_reason : str | None , symbols : list [str ]):
216+ labels = {** self .metrics_labels , "status" : status }
217+ if error_reason :
218+ # don't flag rate limiting as this is expected with redundancy
219+ if error_reason == "rate_limit" :
220+ return
221+ labels ["error_reason" ] = error_reason
222+
223+ for symbol in symbols :
224+ labels ["symbol" ] = symbol
225+ self .metrics .update_attempts_total .add (1 , labels )
226+
227+ def _get_error_reason (self , response ):
228+ response = response .get ("response" )
229+ if not response :
230+ return None
231+ elif "Oracle price update too often" in response :
232+ return PushErrorReason .RATE_LIMIT
233+ elif "Too many cumulative requests" in response :
234+ return PushErrorReason .USER_LIMIT
235+ elif "Invalid nonce" in response :
236+ return PushErrorReason .INVALID_NONCE
237+ else :
238+ logger .warning ("Unrecognized error response: {}" , response )
239+ return PushErrorReason .UNKNOWN
0 commit comments