Skip to main content

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