alloy_rpc_types_eth/
log.rs

1use alloc::vec::Vec;
2use alloy_consensus::transaction::TransactionMeta;
3use alloy_primitives::{Address, BlockHash, LogData, TxHash, B256};
4
5/// Ethereum Log emitted by a transaction
6#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8#[cfg_attr(any(test, feature = "arbitrary"), derive(arbitrary::Arbitrary))]
9#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
10pub struct Log<T = LogData> {
11    #[cfg_attr(feature = "serde", serde(flatten))]
12    /// Consensus log object
13    pub inner: alloy_primitives::Log<T>,
14    /// Hash of the block the transaction that emitted this log was mined in
15    pub block_hash: Option<BlockHash>,
16    /// Number of the block the transaction that emitted this log was mined in
17    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity::opt"))]
18    pub block_number: Option<u64>,
19    /// The timestamp of the block as proposed in:
20    /// <https://ethereum-magicians.org/t/proposal-for-adding-blocktimestamp-to-logs-object-returned-by-eth-getlogs-and-related-requests>
21    /// <https://github.com/ethereum/execution-apis/issues/295>
22    #[cfg_attr(
23        feature = "serde",
24        serde(
25            skip_serializing_if = "Option::is_none",
26            with = "alloy_serde::quantity::opt",
27            default
28        )
29    )]
30    pub block_timestamp: Option<u64>,
31    /// Transaction Hash
32    #[doc(alias = "tx_hash")]
33    pub transaction_hash: Option<TxHash>,
34    /// Index of the Transaction in the block
35    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity::opt"))]
36    #[doc(alias = "tx_index")]
37    pub transaction_index: Option<u64>,
38    /// Log Index in Block
39    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity::opt"))]
40    pub log_index: Option<u64>,
41    /// Geth Compatibility Field: whether this log was removed
42    #[cfg_attr(feature = "serde", serde(default))]
43    pub removed: bool,
44}
45
46impl<T> Log<T> {
47    /// Getter for the address field. Shortcut for `log.inner.address`.
48    pub const fn address(&self) -> Address {
49        self.inner.address
50    }
51
52    /// Getter for the data field. Shortcut for `log.inner.data`.
53    pub const fn data(&self) -> &T {
54        &self.inner.data
55    }
56
57    /// Consumes the type and returns the wrapped [`alloy_primitives::Log`]
58    pub fn into_inner(self) -> alloy_primitives::Log<T> {
59        self.inner
60    }
61}
62
63impl Log<LogData> {
64    /// Getter for the topics field. Shortcut for `log.inner.topics()`.
65    pub fn topics(&self) -> &[B256] {
66        self.inner.topics()
67    }
68
69    /// Getter for the topic0 field.
70    #[doc(alias = "event_signature")]
71    pub fn topic0(&self) -> Option<&B256> {
72        self.inner.topics().first()
73    }
74
75    /// Get the topic list, mutably. This gives access to the internal
76    /// array, without allowing extension of that array. Shortcut for
77    /// [`LogData::topics_mut`]
78    pub fn topics_mut(&mut self) -> &mut [B256] {
79        self.inner.data.topics_mut()
80    }
81
82    /// Decode the log data into a typed log.
83    pub fn log_decode<T: alloy_sol_types::SolEvent>(&self) -> alloy_sol_types::Result<Log<T>> {
84        let decoded = T::decode_log(&self.inner)?;
85        Ok(Log {
86            inner: decoded,
87            block_hash: self.block_hash,
88            block_number: self.block_number,
89            block_timestamp: self.block_timestamp,
90            transaction_hash: self.transaction_hash,
91            transaction_index: self.transaction_index,
92            log_index: self.log_index,
93            removed: self.removed,
94        })
95    }
96
97    /// Decode the log data with validation into a typed log.
98    pub fn log_decode_validate<T: alloy_sol_types::SolEvent>(
99        &self,
100    ) -> alloy_sol_types::Result<Log<T>> {
101        let decoded = T::decode_log_validate(&self.inner)?;
102        Ok(Log {
103            inner: decoded,
104            block_hash: self.block_hash,
105            block_number: self.block_number,
106            block_timestamp: self.block_timestamp,
107            transaction_hash: self.transaction_hash,
108            transaction_index: self.transaction_index,
109            log_index: self.log_index,
110            removed: self.removed,
111        })
112    }
113
114    /// Creates a collection of RPC logs from transaction receipt logs.
115    ///
116    /// This function takes raw consensus logs and enriches them with RPC metadata
117    /// needed for API responses, including block information and proper indexing.
118    ///
119    /// # Arguments
120    ///
121    /// * `previous_log_count` - The total number of logs from previous transactions in the same
122    ///   block. Used to calculate the correct `log_index` for each log.
123    /// * `meta` - Transaction metadata containing block hash, number, timestamp, and transaction
124    ///   information needed to populate the RPC log fields.
125    /// * `logs` - An iterator of consensus logs to be converted into RPC logs.
126    ///
127    /// # Returns
128    ///
129    /// A vector of RPC logs with all metadata fields populated, ready to be included in the
130    /// transaction receipt.
131    pub fn collect_for_receipt<I, T>(
132        previous_log_count: usize,
133        meta: TransactionMeta,
134        logs: I,
135    ) -> Vec<Log<T>>
136    where
137        I: IntoIterator<Item = alloy_primitives::Log<T>>,
138    {
139        logs.into_iter()
140            .enumerate()
141            .map(|(tx_log_idx, log)| Log {
142                inner: log,
143                block_hash: Some(meta.block_hash),
144                block_number: Some(meta.block_number),
145                block_timestamp: Some(meta.timestamp),
146                transaction_hash: Some(meta.tx_hash),
147                transaction_index: Some(meta.index),
148                log_index: Some((previous_log_count + tx_log_idx) as u64),
149                removed: false,
150            })
151            .collect()
152    }
153}
154
155impl<T> alloy_rlp::Encodable for Log<T>
156where
157    for<'a> &'a T: Into<LogData>,
158{
159    fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
160        self.reserialize_inner().encode(out)
161    }
162
163    fn length(&self) -> usize {
164        self.reserialize_inner().length()
165    }
166}
167
168impl<T> Log<T>
169where
170    for<'a> &'a T: Into<LogData>,
171{
172    /// Reserialize the inner data, returning an [`alloy_primitives::Log`].
173    pub fn reserialize_inner(&self) -> alloy_primitives::Log {
174        alloy_primitives::Log { address: self.inner.address, data: (&self.inner.data).into() }
175    }
176
177    /// Reserialize the data, returning a new `Log` object wrapping an
178    /// [`alloy_primitives::Log`]. this copies the log metadata, preserving
179    /// the original object.
180    pub fn reserialize(&self) -> Log<LogData> {
181        Log {
182            inner: self.reserialize_inner(),
183            block_hash: self.block_hash,
184            block_number: self.block_number,
185            block_timestamp: self.block_timestamp,
186            transaction_hash: self.transaction_hash,
187            transaction_index: self.transaction_index,
188            log_index: self.log_index,
189            removed: self.removed,
190        }
191    }
192}
193
194impl<T> AsRef<alloy_primitives::Log<T>> for Log<T> {
195    fn as_ref(&self) -> &alloy_primitives::Log<T> {
196        &self.inner
197    }
198}
199
200impl<T> AsMut<alloy_primitives::Log<T>> for Log<T> {
201    fn as_mut(&mut self) -> &mut alloy_primitives::Log<T> {
202        &mut self.inner
203    }
204}
205
206impl<T> AsRef<T> for Log<T> {
207    fn as_ref(&self) -> &T {
208        &self.inner.data
209    }
210}
211
212impl<T> AsMut<T> for Log<T> {
213    fn as_mut(&mut self) -> &mut T {
214        &mut self.inner.data
215    }
216}
217
218impl<L> From<Log<L>> for alloy_primitives::Log<L> {
219    fn from(value: Log<L>) -> Self {
220        value.into_inner()
221    }
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use alloy_consensus::{Receipt, ReceiptWithBloom, TxReceipt};
228    use alloy_primitives::{Address, Bytes};
229    use arbitrary::Arbitrary;
230    use rand::Rng;
231    use similar_asserts::assert_eq;
232
233    const fn assert_tx_receipt<T: TxReceipt>() {}
234
235    #[test]
236    const fn assert_receipt() {
237        assert_tx_receipt::<ReceiptWithBloom<Receipt<Log>>>();
238    }
239
240    #[test]
241    fn log_arbitrary() {
242        let mut bytes = [0u8; 1024];
243        rand::thread_rng().fill(bytes.as_mut_slice());
244
245        let _: Log = Log::arbitrary(&mut arbitrary::Unstructured::new(&bytes)).unwrap();
246    }
247
248    #[test]
249    #[cfg(feature = "serde")]
250    fn serde_log() {
251        let mut log = Log {
252            inner: alloy_primitives::Log {
253                address: Address::with_last_byte(0x69),
254                data: alloy_primitives::LogData::new_unchecked(
255                    vec![B256::with_last_byte(0x69)],
256                    Bytes::from_static(&[0x69]),
257                ),
258            },
259            block_hash: Some(B256::with_last_byte(0x69)),
260            block_number: Some(0x69),
261            block_timestamp: None,
262            transaction_hash: Some(B256::with_last_byte(0x69)),
263            transaction_index: Some(0x69),
264            log_index: Some(0x69),
265            removed: false,
266        };
267        let serialized = serde_json::to_string(&log).unwrap();
268        assert_eq!(
269            serialized,
270            r#"{"address":"0x0000000000000000000000000000000000000069","topics":["0x0000000000000000000000000000000000000000000000000000000000000069"],"data":"0x69","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000069","blockNumber":"0x69","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000069","transactionIndex":"0x69","logIndex":"0x69","removed":false}"#
271        );
272
273        let deserialized: Log = serde_json::from_str(&serialized).unwrap();
274        assert_eq!(log, deserialized);
275
276        log.block_timestamp = Some(0x69);
277        let serialized = serde_json::to_string(&log).unwrap();
278        assert_eq!(
279            serialized,
280            r#"{"address":"0x0000000000000000000000000000000000000069","topics":["0x0000000000000000000000000000000000000000000000000000000000000069"],"data":"0x69","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000069","blockNumber":"0x69","blockTimestamp":"0x69","transactionHash":"0x0000000000000000000000000000000000000000000000000000000000000069","transactionIndex":"0x69","logIndex":"0x69","removed":false}"#
281        );
282
283        let deserialized: Log = serde_json::from_str(&serialized).unwrap();
284        assert_eq!(log, deserialized);
285    }
286}