Skip to main content

evm_oracle_state/
events.rs

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
7/// `AnswerUpdated(int256,uint256,uint256)` topic0.
8pub const ANSWER_UPDATED_TOPIC: B256 =
9    b256!("0559884fd3a460db3073b7fc896cc77986f16e378210ded43186175bf646fc5f");
10
11/// `NewRound(uint256,address,uint256)` topic0.
12pub const NEW_ROUND_TOPIC: B256 =
13    b256!("0109fc6f55cf40689f02fbaad7af7fe7bbac8a3d2186600afc7d3e10cac60271");
14
15/// OCR2 `NewTransmission(uint32,int192,address,uint32,int192[],bytes,int192,bytes32,uint40)` topic0.
16pub const OCR2_NEW_TRANSMISSION_TOPIC: B256 =
17    b256!("c797025feeeaf2cd924c99e9205acb8ec04d5cad21c41ce637a38fb6dee6016a");
18
19/// OCR1 `NewTransmission(uint32,int192,address,int192[],bytes,bytes32)` topic0.
20pub const OCR1_NEW_TRANSMISSION_TOPIC: B256 =
21    b256!("f6a97944f31ea060dfde0566e4167c1a1082551e64b60ecb14d599a9d023d451");
22
23/// Pyth `PriceFeedUpdate(bytes32,uint64,int64,uint64)` topic0.
24#[cfg(feature = "pyth")]
25pub const PYTH_PRICE_FEED_UPDATE_TOPIC: B256 =
26    b256!("d06a6b7f4918494b3719217d1802786c1f5112a6c1d88fe2cfec00b4584f6aec");
27
28/// RedStone `ValueUpdate(uint256,bytes32,uint256)` topic0.
29#[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/// Decoded Chainlink OCR1 `NewTransmission` event.
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct Ocr1NewTransmission {
65    /// Emitting aggregator address.
66    pub aggregator: Address,
67    /// Aggregator-local round id.
68    pub aggregator_round_id: U256,
69    /// OCR1 median answer.
70    pub answer: I256,
71    /// Address that transmitted the report.
72    pub transmitter: Address,
73    /// OCR1 raw report context.
74    pub raw_report_context: B256,
75    /// OCR1 config digest extracted from `rawReportContext`.
76    pub config_digest: FixedBytes<16>,
77    /// OCR1 epoch and round packed as uint40.
78    pub epoch_and_round: u64,
79    /// Log block number, when present.
80    pub block_number: Option<u64>,
81    /// Log index, when present.
82    pub log_index: Option<u64>,
83    /// Whether the provider marked the log as removed.
84    pub removed: bool,
85}
86
87/// Decoded Chainlink `AnswerUpdated` event.
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct AnswerUpdated {
90    /// Emitting aggregator address.
91    pub aggregator: Address,
92    /// Signed answer from the event.
93    pub current: I256,
94    /// Aggregator-local round id.
95    pub round_id: U256,
96    /// Event `updatedAt` timestamp.
97    pub updated_at: u64,
98    /// Log block number, when present.
99    pub block_number: Option<u64>,
100    /// Log index, when present.
101    pub log_index: Option<u64>,
102    /// Whether the provider marked the log as removed.
103    pub removed: bool,
104}
105
106/// Decoded Chainlink OCR2 `NewTransmission` event.
107#[derive(Clone, Debug, PartialEq, Eq)]
108pub struct Ocr2NewTransmission {
109    /// Emitting aggregator address.
110    pub aggregator: Address,
111    /// Aggregator-local round id.
112    pub aggregator_round_id: U256,
113    /// OCR2 median answer.
114    pub answer: I256,
115    /// Address that transmitted the report.
116    pub transmitter: Address,
117    /// OCR2 observations timestamp, used as `startedAt`.
118    pub observations_timestamp: u64,
119    /// OCR2 config digest for the transmission.
120    pub config_digest: B256,
121    /// OCR2 epoch and round packed as uint40.
122    pub epoch_and_round: u64,
123    /// Log block number, when present.
124    pub block_number: Option<u64>,
125    /// Log index, when present.
126    pub log_index: Option<u64>,
127    /// Whether the provider marked the log as removed.
128    pub removed: bool,
129}
130
131/// Decoded Chainlink `NewRound` event.
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct NewRound {
134    /// Emitting aggregator address.
135    pub aggregator: Address,
136    /// Aggregator-local round id.
137    pub round_id: U256,
138    /// Address that started the round.
139    pub started_by: Address,
140    /// Event `startedAt` timestamp.
141    pub started_at: u64,
142    /// Log block number, when present.
143    pub block_number: Option<u64>,
144    /// Log index, when present.
145    pub log_index: Option<u64>,
146    /// Whether the provider marked the log as removed.
147    pub removed: bool,
148}
149
150/// Decoded Pyth `PriceFeedUpdate` event.
151#[cfg(feature = "pyth")]
152#[derive(Clone, Debug, PartialEq, Eq)]
153pub struct PythPriceFeedUpdate {
154    /// Emitting Pyth contract address.
155    pub pyth: Address,
156    /// Pyth price feed id.
157    pub price_id: B256,
158    /// Pyth publish timestamp.
159    pub publish_time: u64,
160    /// Fixed-point price value.
161    pub price: i64,
162    /// Pyth confidence interval.
163    pub conf: u64,
164    /// Log block number, when present.
165    pub block_number: Option<u64>,
166    /// Log index, when present.
167    pub log_index: Option<u64>,
168    /// Whether the provider marked the log as removed.
169    pub removed: bool,
170}
171
172/// Decoded RedStone `ValueUpdate` event.
173#[cfg(feature = "redstone")]
174#[derive(Clone, Debug, PartialEq, Eq)]
175pub struct RedstoneValueUpdate {
176    /// Emitting RedStone adapter address.
177    pub adapter: Address,
178    /// RedStone data feed id.
179    pub data_feed_id: B256,
180    /// Unsigned RedStone value.
181    pub value: U256,
182    /// Event `updatedAt` timestamp.
183    pub updated_at: u64,
184    /// Log block number, when present.
185    pub block_number: Option<u64>,
186    /// Log index, when present.
187    pub log_index: Option<u64>,
188    /// Whether the provider marked the log as removed.
189    pub removed: bool,
190}
191
192/// Decode a Chainlink `AnswerUpdated` log.
193pub 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/// Decode a RedStone `ValueUpdate` log.
209#[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
238/// Decode a Chainlink OCR1 `NewTransmission` log.
239pub 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
265/// Decode a Chainlink OCR2 `NewTransmission` log.
266pub 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
292/// Decode a Chainlink `NewRound` log.
293pub 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/// Decode a Pyth `PriceFeedUpdate` log.
309#[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}