1use alloy_primitives::{Address, B256, FixedBytes, I256, U256, b256};
2use alloy_rpc_types_eth::Log as RpcLog;
3use alloy_sol_types::SolEvent;
4
5use crate::ChainlinkEventDecodeError;
6
7pub const ANSWER_UPDATED_TOPIC: B256 =
9 b256!("0559884fd3a460db3073b7fc896cc77986f16e378210ded43186175bf646fc5f");
10
11pub const NEW_ROUND_TOPIC: B256 =
13 b256!("0109fc6f55cf40689f02fbaad7af7fe7bbac8a3d2186600afc7d3e10cac60271");
14
15pub const OCR2_NEW_TRANSMISSION_TOPIC: B256 =
17 b256!("c797025feeeaf2cd924c99e9205acb8ec04d5cad21c41ce637a38fb6dee6016a");
18
19pub const OCR1_NEW_TRANSMISSION_TOPIC: B256 =
21 b256!("f6a97944f31ea060dfde0566e4167c1a1082551e64b60ecb14d599a9d023d451");
22
23#[cfg(feature = "pyth")]
25pub const PYTH_PRICE_FEED_UPDATE_TOPIC: B256 =
26 b256!("d06a6b7f4918494b3719217d1802786c1f5112a6c1d88fe2cfec00b4584f6aec");
27
28#[cfg(feature = "redstone")]
30pub const REDSTONE_VALUE_UPDATE_TOPIC: B256 =
31 b256!("f36866d965ee70c8632ff558f5cf8d41ee9ca1d0d0bc7700786e57be60747390");
32
33mod ocr1_abi {
34 alloy_sol_types::sol! {
35 event NewTransmission(
36 uint32 indexed aggregatorRoundId,
37 int192 answer,
38 address transmitter,
39 int192[] observations,
40 bytes observers,
41 bytes32 rawReportContext
42 );
43 }
44}
45
46mod ocr2_abi {
47 alloy_sol_types::sol! {
48 event NewTransmission(
49 uint32 indexed aggregatorRoundId,
50 int192 answer,
51 address transmitter,
52 uint32 observationsTimestamp,
53 int192[] observations,
54 bytes observers,
55 int192 juelsPerFeeCoin,
56 bytes32 configDigest,
57 uint40 epochAndRound
58 );
59 }
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct Ocr1NewTransmission {
65 pub aggregator: Address,
67 pub aggregator_round_id: U256,
69 pub answer: I256,
71 pub transmitter: Address,
73 pub raw_report_context: B256,
75 pub config_digest: FixedBytes<16>,
77 pub epoch_and_round: u64,
79 pub block_number: Option<u64>,
81 pub log_index: Option<u64>,
83 pub removed: bool,
85}
86
87#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct AnswerUpdated {
90 pub aggregator: Address,
92 pub current: I256,
94 pub round_id: U256,
96 pub updated_at: u64,
98 pub block_number: Option<u64>,
100 pub log_index: Option<u64>,
102 pub removed: bool,
104}
105
106#[derive(Clone, Debug, PartialEq, Eq)]
108pub struct Ocr2NewTransmission {
109 pub aggregator: Address,
111 pub aggregator_round_id: U256,
113 pub answer: I256,
115 pub transmitter: Address,
117 pub observations_timestamp: u64,
119 pub config_digest: B256,
121 pub epoch_and_round: u64,
123 pub block_number: Option<u64>,
125 pub log_index: Option<u64>,
127 pub removed: bool,
129}
130
131#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct NewRound {
134 pub aggregator: Address,
136 pub round_id: U256,
138 pub started_by: Address,
140 pub started_at: u64,
142 pub block_number: Option<u64>,
144 pub log_index: Option<u64>,
146 pub removed: bool,
148}
149
150#[cfg(feature = "pyth")]
152#[derive(Clone, Debug, PartialEq, Eq)]
153pub struct PythPriceFeedUpdate {
154 pub pyth: Address,
156 pub price_id: B256,
158 pub publish_time: u64,
160 pub price: i64,
162 pub conf: u64,
164 pub block_number: Option<u64>,
166 pub log_index: Option<u64>,
168 pub removed: bool,
170}
171
172#[cfg(feature = "redstone")]
174#[derive(Clone, Debug, PartialEq, Eq)]
175pub struct RedstoneValueUpdate {
176 pub adapter: Address,
178 pub data_feed_id: B256,
180 pub value: U256,
182 pub updated_at: u64,
184 pub block_number: Option<u64>,
186 pub log_index: Option<u64>,
188 pub removed: bool,
190}
191
192pub fn decode_answer_updated(log: &RpcLog) -> Result<AnswerUpdated, ChainlinkEventDecodeError> {
194 require_topic(log, ANSWER_UPDATED_TOPIC)?;
195 require_topic_count(log, 3)?;
196 let data = data_word(log, "updated_at")?;
197 Ok(AnswerUpdated {
198 aggregator: log.address(),
199 current: topic_i256(log.topics()[1]),
200 round_id: topic_u256(log.topics()[2]),
201 updated_at: u256_to_u64(data, "updated_at")?,
202 block_number: log.block_number,
203 log_index: log.log_index,
204 removed: log.removed,
205 })
206}
207
208#[cfg(feature = "redstone")]
210pub fn decode_redstone_value_update(
211 log: &RpcLog,
212) -> Result<RedstoneValueUpdate, ChainlinkEventDecodeError> {
213 require_topic(log, REDSTONE_VALUE_UPDATE_TOPIC)?;
214 require_topic_count(log, 1)?;
215 let data = log.inner.data.data.as_ref();
216 if data.len() != 96 {
217 return Err(ChainlinkEventDecodeError::WrongDataLength {
218 expected: 96,
219 actual: data.len(),
220 });
221 }
222
223 let value = U256::from_be_slice(&data[0..32]);
224 let data_feed_id = B256::from_slice(&data[32..64]);
225 let updated_at = u256_to_u64(U256::from_be_slice(&data[64..96]), "updated_at")?;
226
227 Ok(RedstoneValueUpdate {
228 adapter: log.address(),
229 data_feed_id,
230 value,
231 updated_at,
232 block_number: log.block_number,
233 log_index: log.log_index,
234 removed: log.removed,
235 })
236}
237
238pub fn decode_ocr1_new_transmission(
240 log: &RpcLog,
241) -> Result<Ocr1NewTransmission, ChainlinkEventDecodeError> {
242 require_topic(log, OCR1_NEW_TRANSMISSION_TOPIC)?;
243 require_topic_count(log, 2)?;
244 let decoded = ocr1_abi::NewTransmission::decode_log_validate(&log.inner).map_err(|error| {
245 ChainlinkEventDecodeError::AbiDecode {
246 event: "NewTransmission",
247 message: error.to_string(),
248 }
249 })?;
250 let data = decoded.data;
251 Ok(Ocr1NewTransmission {
252 aggregator: log.address(),
253 aggregator_round_id: U256::from(data.aggregatorRoundId),
254 answer: int192_to_i256(data.answer),
255 transmitter: data.transmitter,
256 raw_report_context: data.rawReportContext,
257 config_digest: ocr1_config_digest(data.rawReportContext),
258 epoch_and_round: u40_from_context(data.rawReportContext),
259 block_number: log.block_number,
260 log_index: log.log_index,
261 removed: log.removed,
262 })
263}
264
265pub fn decode_ocr2_new_transmission(
267 log: &RpcLog,
268) -> Result<Ocr2NewTransmission, ChainlinkEventDecodeError> {
269 require_topic(log, OCR2_NEW_TRANSMISSION_TOPIC)?;
270 require_topic_count(log, 2)?;
271 let decoded = ocr2_abi::NewTransmission::decode_log_validate(&log.inner).map_err(|error| {
272 ChainlinkEventDecodeError::AbiDecode {
273 event: "NewTransmission",
274 message: error.to_string(),
275 }
276 })?;
277 let data = decoded.data;
278 Ok(Ocr2NewTransmission {
279 aggregator: log.address(),
280 aggregator_round_id: U256::from(data.aggregatorRoundId),
281 answer: int192_to_i256(data.answer),
282 transmitter: data.transmitter,
283 observations_timestamp: u64::from(data.observationsTimestamp),
284 config_digest: data.configDigest,
285 epoch_and_round: data.epochAndRound.as_limbs()[0],
286 block_number: log.block_number,
287 log_index: log.log_index,
288 removed: log.removed,
289 })
290}
291
292pub fn decode_new_round(log: &RpcLog) -> Result<NewRound, ChainlinkEventDecodeError> {
294 require_topic(log, NEW_ROUND_TOPIC)?;
295 require_topic_count(log, 3)?;
296 let data = data_word(log, "started_at")?;
297 Ok(NewRound {
298 aggregator: log.address(),
299 round_id: topic_u256(log.topics()[1]),
300 started_by: topic_address(log.topics()[2]),
301 started_at: u256_to_u64(data, "started_at")?,
302 block_number: log.block_number,
303 log_index: log.log_index,
304 removed: log.removed,
305 })
306}
307
308#[cfg(feature = "pyth")]
310pub fn decode_pyth_price_feed_update(
311 log: &RpcLog,
312) -> Result<PythPriceFeedUpdate, ChainlinkEventDecodeError> {
313 require_topic(log, PYTH_PRICE_FEED_UPDATE_TOPIC)?;
314 require_topic_count(log, 2)?;
315 let data = log.inner.data.data.as_ref();
316 if data.len() != 96 {
317 return Err(ChainlinkEventDecodeError::WrongDataLength {
318 expected: 96,
319 actual: data.len(),
320 });
321 }
322
323 let publish_time = u256_to_u64(U256::from_be_slice(&data[0..32]), "publish_time")?;
324 let price = int64_word(&data[32..64])?;
325 let conf = u256_to_u64(U256::from_be_slice(&data[64..96]), "conf")?;
326
327 Ok(PythPriceFeedUpdate {
328 pyth: log.address(),
329 price_id: log.topics()[1],
330 publish_time,
331 price,
332 conf,
333 block_number: log.block_number,
334 log_index: log.log_index,
335 removed: log.removed,
336 })
337}
338
339fn require_topic(log: &RpcLog, expected: B256) -> Result<(), ChainlinkEventDecodeError> {
340 let actual = log.topics().first().copied();
341 if actual == Some(expected) {
342 Ok(())
343 } else {
344 Err(ChainlinkEventDecodeError::WrongTopic { expected, actual })
345 }
346}
347
348fn require_topic_count(log: &RpcLog, expected: usize) -> Result<(), ChainlinkEventDecodeError> {
349 let actual = log.topics().len();
350 if actual == expected {
351 Ok(())
352 } else {
353 Err(ChainlinkEventDecodeError::WrongTopicCount { expected, actual })
354 }
355}
356
357fn data_word(log: &RpcLog, field: &'static str) -> Result<U256, ChainlinkEventDecodeError> {
358 let data = log.inner.data.data.as_ref();
359 if data.len() != 32 {
360 return Err(ChainlinkEventDecodeError::WrongDataLength {
361 expected: 32,
362 actual: data.len(),
363 });
364 }
365
366 let _ = field;
367 Ok(U256::from_be_slice(data))
368}
369
370fn topic_u256(topic: B256) -> U256 {
371 U256::from_be_slice(topic.as_slice())
372}
373
374fn topic_i256(topic: B256) -> I256 {
375 I256::from_raw(topic_u256(topic))
376}
377
378fn topic_address(topic: B256) -> Address {
379 Address::from_slice(&topic.as_slice()[12..])
380}
381
382fn int192_to_i256(value: alloy_primitives::Signed<192, 3>) -> I256 {
383 let raw = value.into_raw();
384 let mut limbs = [0_u64; 4];
385 limbs[..3].copy_from_slice(raw.as_limbs());
386 if raw.as_limbs()[2] & (1_u64 << 63) != 0 {
387 limbs[3] = u64::MAX;
388 }
389 I256::from_raw(U256::from_limbs(limbs))
390}
391
392fn ocr1_config_digest(raw_report_context: B256) -> FixedBytes<16> {
393 let mut digest = [0_u8; 16];
394 digest.copy_from_slice(&raw_report_context.as_slice()[11..27]);
395 FixedBytes::from(digest)
396}
397
398fn u40_from_context(raw_report_context: B256) -> u64 {
399 let bytes = raw_report_context.as_slice();
400 u64::from_be_bytes([
401 0, 0, 0, bytes[27], bytes[28], bytes[29], bytes[30], bytes[31],
402 ])
403}
404
405#[cfg(feature = "pyth")]
406fn int64_word(word: &[u8]) -> Result<i64, ChainlinkEventDecodeError> {
407 debug_assert_eq!(word.len(), 32);
408 let negative = word[24] & 0x80 != 0;
409 let expected_prefix = if negative { 0xff } else { 0x00 };
410 if word[..24].iter().any(|byte| *byte != expected_prefix) {
411 return Err(ChainlinkEventDecodeError::AbiDecode {
412 event: "PriceFeedUpdate",
413 message: "price does not fit int64".to_string(),
414 });
415 }
416 let mut bytes = [0_u8; 8];
417 bytes.copy_from_slice(&word[24..32]);
418 Ok(i64::from_be_bytes(bytes))
419}
420
421fn u256_to_u64(value: U256, field: &'static str) -> Result<u64, ChainlinkEventDecodeError> {
422 u64::try_from(value).map_err(|_| ChainlinkEventDecodeError::Uint64Overflow { field, value })
423}