3838STREAM_RESTART_DELAY = 5
3939
4040
41+ def _decode_v2_report_data (report_blob : bytes ) -> dict :
42+ """Decode v2 report blob (TWAP feeds). py_chainlink_streams only supports v3."""
43+ from eth_abi import decode # pyright: ignore[reportMissingImports]
44+
45+ types = [
46+ "bytes32" , # feedId
47+ "uint32" , # validFromTimestamp
48+ "uint32" , # observationsTimestamp
49+ "uint192" , # nativeFee
50+ "uint192" , # linkFee
51+ "uint32" , # expiresAt
52+ "int192" , # benchmarkPrice (TWAP)
53+ ]
54+ decoded = decode (types , report_blob )
55+ return {
56+ "observationsTimestamp" : decoded [2 ],
57+ "benchmarkPrice" : decoded [6 ],
58+ }
59+
60+
61+ def _extract_report_price (report : "ReportResponse" ) -> tuple [float , Optional [int ]]:
62+ """Decode benchmark (v3) or TWAP (v2) price from a Chainlink report."""
63+ schema = ReportResponse .get_schema_version (report .feed_id )
64+ if schema == 3 :
65+ prices = report .get_decoded_prices ()
66+ obs_ts = prices .get ("observationsTimestamp" )
67+ return float (prices .get ("benchmarkPrice" , 0.0 )), int (obs_ts ) if obs_ts is not None else None
68+ if schema == 2 :
69+ structure = ReportResponse ._decode_report_structure (report .full_report )
70+ data = _decode_v2_report_data (structure ["reportBlob" ])
71+ price = ReportResponse .convert_fixed_point_to_decimal (data ["benchmarkPrice" ])
72+ return price , int (data .get ("observationsTimestamp" , report .observations_timestamp ))
73+ raise ValueError (f"Unsupported Chainlink report schema v{ schema } for feed { report .feed_id } " )
74+
75+
4176class ChainlinkClient :
42- def __init__ (self , feed_id : str , start_stream_thread : bool = True ):
77+ def __init__ (
78+ self ,
79+ feed_id : str ,
80+ start_stream_thread : bool = True ,
81+ price_config_key : str = "CURRENT_PRICE" ,
82+ ):
4383 self .streams_config = None
4484 self .client = None
4585 self .feed_id = feed_id
4686 self .feed_ids = [feed_id ] if feed_id else []
87+ self .price_config_key = price_config_key
4788 self ._price_history : deque = deque (maxlen = 3000 )
4889 self ._last_price : Optional [float ] = None
4990 self ._last_update_time : Optional [int ] = None
@@ -68,9 +109,20 @@ def __init__(self, feed_id: str, start_stream_thread: bool = True):
68109 else :
69110 logger .warning ("Chainlink streams client unavailable; stream features are disabled." )
70111
71- if start_stream_thread and self .client is not None :
72- self .thread = threading .Thread (target = self ._stream , daemon = True )
112+ if not self .feed_id :
113+ logger .warning (
114+ "Chainlink feed_id missing for %s; stream and historical fetch disabled." ,
115+ self .price_config_key ,
116+ )
117+
118+ if start_stream_thread and self .client is not None and self .feed_id :
119+ self .thread = threading .Thread (
120+ target = self ._stream ,
121+ daemon = True ,
122+ name = f"chainlink-{ self .price_config_key } " ,
123+ )
73124 self .thread .start ()
125+
74126 def _price_at_ago_ms (self , history : List [Tuple [int , float ]], now_ms : int , ago_ms : int ) -> Optional [float ]:
75127 """Return price from history closest to (now_ms - ago_ms)."""
76128 target = now_ms - ago_ms
@@ -85,7 +137,6 @@ def _price_at_ago_ms(self, history: List[Tuple[int, float]], now_ms: int, ago_ms
85137 best_price = price
86138 return best_price
87139
88-
89140 @property
90141 def last_price (self ) -> Optional [float ]:
91142 """Last received trade price, or None if no update yet."""
@@ -115,8 +166,8 @@ def get_price_at_timestamp(self, timestamp: int | float | str) -> float:
115166 logger .warning ("Invalid timestamp for Chainlink report: %r" , timestamp )
116167 return 0.0
117168 report = self .client .get_report (self .feed_id , ts_int )
118- prices = report . get_decoded_prices ( )
119- return float ( prices . get ( "benchmarkPrice" , 0.0 ))
169+ price , _obs_ts = _extract_report_price ( report )
170+ return price
120171
121172 def _stream (self ) -> None :
122173 if self .client is None :
@@ -142,14 +193,20 @@ def _stream(self) -> None:
142193
143194 def _on_connection_status (self , is_connected : bool , host : str , _origin : str ) -> None :
144195 if is_connected :
145- logger .info ("Chainlink stream connected to %s" , host )
196+ logger .info ("Chainlink stream connected to %s (%s) " , host , self . price_config_key )
146197 else :
147- logger .warning ("Chainlink stream disconnected from %s" , host )
198+ logger .warning ("Chainlink stream disconnected from %s (%s) " , host , self . price_config_key )
148199
149200 async def _process_report (self , report_data : dict ) -> None :
150201 if ReportResponse is None :
151202 return
152- report = ReportResponse .from_dict (report_data )
153- prices = report .get_decoded_prices ()
154- price = float (prices .get ("benchmarkPrice" , 0.0 ))
155- config .CURRENT_PRICE = price
203+ try :
204+ report = ReportResponse .from_dict (report_data )
205+ price , obs_ts = _extract_report_price (report )
206+ except Exception :
207+ logger .exception ("Failed to decode Chainlink report for %s" , self .price_config_key )
208+ return
209+ self ._last_price = price
210+ if obs_ts is not None :
211+ self ._last_update_time = int (obs_ts ) * 1000
212+ setattr (config , self .price_config_key , price )
0 commit comments