hypersync_net_types/
block.rs

1use crate::{hypersync_net_types_capnp, types::AnyOf, CapnpBuilder, CapnpReader, Selection};
2use anyhow::Context;
3use hypersync_format::{Address, Hash};
4use serde::{Deserialize, Serialize};
5use std::collections::BTreeSet;
6
7pub type BlockSelection = Selection<BlockFilter>;
8
9impl From<BlockFilter> for AnyOf<BlockFilter> {
10    fn from(filter: BlockFilter) -> Self {
11        Self::new(filter)
12    }
13}
14
15#[derive(Default, Serialize, Deserialize, Clone, Debug, PartialEq)]
16pub struct BlockFilter {
17    /// Hash of a block, any blocks that have one of these hashes will be returned.
18    /// Empty means match all.
19    #[serde(default, skip_serializing_if = "Vec::is_empty")]
20    pub hash: Vec<Hash>,
21    /// Miner address of a block, any blocks that have one of these miners will be returned.
22    /// Empty means match all.
23    #[serde(default, skip_serializing_if = "Vec::is_empty")]
24    pub miner: Vec<Address>,
25}
26
27impl BlockFilter {
28    /// Create a block filter that matches all blocks.
29    ///
30    /// This creates an empty filter with no constraints, which will match all blocks.
31    /// You can then use the builder methods to add specific filtering criteria eg.
32    /// `BlockFilter::all().and_miner(["0xdac17f958d2ee523a2206206994597c13d831ec7"])`
33    pub fn all() -> Self {
34        Default::default()
35    }
36
37    /// Combine this filter with another using logical OR.
38    ///
39    /// Creates an `AnyOf` that matches blocks satisfying either this filter or the other filter.
40    /// This allows for fluent chaining of multiple block filters with OR semantics.
41    ///
42    /// # Arguments
43    /// * `other` - Another `BlockFilter` to combine with this one
44    ///
45    /// # Returns
46    /// An `AnyOf<BlockFilter>` that matches blocks satisfying either filter
47    ///
48    /// # Examples
49    ///
50    /// ```
51    /// use hypersync_net_types::BlockFilter;
52    ///
53    /// // Match blocks from specific miners OR with specific hashes
54    /// let filter = BlockFilter::all()
55    ///     .and_miner(["0x1234567890123456789012345678901234567890"])?
56    ///     .or(
57    ///         BlockFilter::all()
58    ///             .and_hash(["0x40d008f2a1653f09b7b028d30c7fd1ba7c84900fcfb032040b3eb3d16f84d294"])?
59    ///     );
60    /// # Ok::<(), anyhow::Error>(())
61    /// ```
62    pub fn or(self, other: Self) -> AnyOf<Self> {
63        AnyOf::new(self).or(other)
64    }
65
66    /// Filter blocks by any of the provided block hashes.
67    ///
68    /// This method accepts any iterable of values that can be converted to `Hash`.
69    /// Common input types include string slices, byte arrays, and `Hash` objects.
70    ///
71    /// # Arguments
72    /// * `hashes` - An iterable of block hashes to filter by
73    ///
74    /// # Returns
75    /// * `Ok(Self)` - The updated filter on success
76    /// * `Err(anyhow::Error)` - If any hash fails to convert
77    ///
78    /// # Examples
79    ///
80    /// ```
81    /// use hypersync_net_types::BlockFilter;
82    ///
83    /// // Filter by a single block hash
84    /// let filter = BlockFilter::all()
85    ///     .and_hash(["0x40d008f2a1653f09b7b028d30c7fd1ba7c84900fcfb032040b3eb3d16f84d294"])?;
86    ///
87    /// // Filter by multiple block hashes
88    /// let filter = BlockFilter::all()
89    ///     .and_hash([
90    ///         "0x40d008f2a1653f09b7b028d30c7fd1ba7c84900fcfb032040b3eb3d16f84d294",
91    ///         "0x88e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6",
92    ///     ])?;
93    ///
94    /// // Using byte arrays
95    /// let block_hash = [
96    ///     0x40, 0xd0, 0x08, 0xf2, 0xa1, 0x65, 0x3f, 0x09, 0xb7, 0xb0, 0x28, 0xd3, 0x0c, 0x7f, 0xd1, 0xba,
97    ///     0x7c, 0x84, 0x90, 0x0f, 0xcf, 0xb0, 0x32, 0x04, 0x0b, 0x3e, 0xb3, 0xd1, 0x6f, 0x84, 0xd2, 0x94
98    /// ];
99    /// let filter = BlockFilter::all()
100    ///     .and_hash([block_hash])?;
101    /// # Ok::<(), anyhow::Error>(())
102    /// ```
103    pub fn and_hash<I, A>(mut self, hashes: I) -> anyhow::Result<Self>
104    where
105        I: IntoIterator<Item = A>,
106        A: TryInto<Hash>,
107        A::Error: std::error::Error + Send + Sync + 'static,
108    {
109        let mut converted_hashes: Vec<Hash> = Vec::new();
110        for (idx, hash) in hashes.into_iter().enumerate() {
111            converted_hashes.push(
112                hash.try_into()
113                    .with_context(|| format!("invalid block hash value at position {idx}"))?,
114            );
115        }
116        self.hash = converted_hashes;
117        Ok(self)
118    }
119
120    /// Filter blocks by any of the provided miner addresses.
121    ///
122    /// This method accepts any iterable of values that can be converted to `Address`.
123    /// Common input types include string slices, byte arrays, and `Address` objects.
124    ///
125    /// # Arguments
126    /// * `addresses` - An iterable of miner addresses to filter by
127    ///
128    /// # Returns
129    /// * `Ok(Self)` - The updated filter on success
130    /// * `Err(anyhow::Error)` - If any address fails to convert
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use hypersync_net_types::BlockFilter;
136    ///
137    /// // Filter by a single miner address
138    /// let filter = BlockFilter::all()
139    ///     .and_miner(["0xdac17f958d2ee523a2206206994597c13d831ec7"])?;
140    ///
141    /// // Filter by multiple miner addresses (e.g., major mining pools)
142    /// let filter = BlockFilter::all()
143    ///     .and_miner([
144    ///         "0xdac17f958d2ee523a2206206994597c13d831ec7", // Pool 1
145    ///         "0xa0b86a33e6c11c8c0c5c0b5e6adee30d1a234567", // Pool 2
146    ///     ])?;
147    ///
148    /// // Using byte arrays
149    /// let miner_address = [
150    ///     0xda, 0xc1, 0x7f, 0x95, 0x8d, 0x2e, 0xe5, 0x23, 0xa2, 0x20,
151    ///     0x62, 0x06, 0x99, 0x45, 0x97, 0xc1, 0x3d, 0x83, 0x1e, 0xc7
152    /// ];
153    /// let filter = BlockFilter::all()
154    ///     .and_miner([miner_address])?;
155    ///
156    /// // Chain with other filter methods
157    /// let filter = BlockFilter::all()
158    ///     .and_hash(["0x40d008f2a1653f09b7b028d30c7fd1ba7c84900fcfb032040b3eb3d16f84d294"])?
159    ///     .and_miner(["0xdac17f958d2ee523a2206206994597c13d831ec7"])?;
160    /// # Ok::<(), anyhow::Error>(())
161    /// ```
162    pub fn and_miner<I, A>(mut self, addresses: I) -> anyhow::Result<Self>
163    where
164        I: IntoIterator<Item = A>,
165        A: TryInto<Address>,
166        A::Error: std::error::Error + Send + Sync + 'static,
167    {
168        let mut converted_addresses: Vec<Address> = Vec::new();
169        for (idx, address) in addresses.into_iter().enumerate() {
170            converted_addresses.push(
171                address
172                    .try_into()
173                    .with_context(|| format!("invalid miner address value at position {idx}"))?,
174            );
175        }
176        self.miner = converted_addresses;
177        Ok(self)
178    }
179}
180
181impl CapnpReader<hypersync_net_types_capnp::block_filter::Owned> for BlockFilter {
182    /// Deserialize BlockSelection from Cap'n Proto reader
183    fn from_reader(
184        reader: hypersync_net_types_capnp::block_filter::Reader,
185    ) -> Result<Self, capnp::Error> {
186        let mut hash = Vec::new();
187
188        // Parse hashes
189        if reader.has_hash() {
190            let hash_list = reader.get_hash()?;
191            for i in 0..hash_list.len() {
192                let hash_data = hash_list.get(i)?;
193                if hash_data.len() == 32 {
194                    let mut hash_bytes = [0u8; 32];
195                    hash_bytes.copy_from_slice(hash_data);
196                    hash.push(Hash::from(hash_bytes));
197                }
198            }
199        }
200
201        let mut miner = Vec::new();
202
203        // Parse miners
204        if reader.has_miner() {
205            let miner_list = reader.get_miner()?;
206            for i in 0..miner_list.len() {
207                let addr_data = miner_list.get(i)?;
208                if addr_data.len() == 20 {
209                    let mut addr_bytes = [0u8; 20];
210                    addr_bytes.copy_from_slice(addr_data);
211                    miner.push(Address::from(addr_bytes));
212                }
213            }
214        }
215
216        Ok(Self { hash, miner })
217    }
218}
219
220impl CapnpBuilder<hypersync_net_types_capnp::block_filter::Owned> for BlockFilter {
221    fn populate_builder(
222        &self,
223        builder: &mut hypersync_net_types_capnp::block_filter::Builder,
224    ) -> Result<(), capnp::Error> {
225        // Set hashes
226        if !self.hash.is_empty() {
227            let mut hash_list = builder.reborrow().init_hash(self.hash.len() as u32);
228            for (i, hash) in self.hash.iter().enumerate() {
229                hash_list.set(i as u32, hash.as_slice());
230            }
231        }
232
233        // Set miners
234        if !self.miner.is_empty() {
235            let mut miner_list = builder.reborrow().init_miner(self.miner.len() as u32);
236            for (i, miner) in self.miner.iter().enumerate() {
237                miner_list.set(i as u32, miner.as_slice());
238            }
239        }
240
241        Ok(())
242    }
243}
244
245#[derive(
246    Debug,
247    Clone,
248    Copy,
249    Serialize,
250    Deserialize,
251    PartialEq,
252    Eq,
253    schemars::JsonSchema,
254    strum_macros::EnumIter,
255    strum_macros::AsRefStr,
256    strum_macros::Display,
257    strum_macros::EnumString,
258)]
259#[serde(rename_all = "snake_case")]
260#[strum(serialize_all = "snake_case")]
261pub enum BlockField {
262    // Non-nullable fields (required)
263    Number,
264    Hash,
265    ParentHash,
266    Sha3Uncles,
267    LogsBloom,
268    TransactionsRoot,
269    StateRoot,
270    ReceiptsRoot,
271    Miner,
272    ExtraData,
273    Size,
274    GasLimit,
275    GasUsed,
276    Timestamp,
277    MixHash,
278
279    // Nullable fields (optional)
280    Nonce,
281    Difficulty,
282    TotalDifficulty,
283    Uncles,
284    BaseFeePerGas,
285    BlobGasUsed,
286    ExcessBlobGas,
287    ParentBeaconBlockRoot,
288    WithdrawalsRoot,
289    Withdrawals,
290    L1BlockNumber,
291    SendCount,
292    SendRoot,
293}
294
295impl Ord for BlockField {
296    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
297        self.as_ref().cmp(other.as_ref())
298    }
299}
300
301impl PartialOrd for BlockField {
302    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
303        Some(self.cmp(other))
304    }
305}
306
307impl BlockField {
308    pub fn all() -> BTreeSet<Self> {
309        use strum::IntoEnumIterator;
310        Self::iter().collect()
311    }
312
313    /// Convert BlockField to Cap'n Proto enum
314    pub fn to_capnp(&self) -> crate::hypersync_net_types_capnp::BlockField {
315        match self {
316            BlockField::Number => crate::hypersync_net_types_capnp::BlockField::Number,
317            BlockField::Hash => crate::hypersync_net_types_capnp::BlockField::Hash,
318            BlockField::ParentHash => crate::hypersync_net_types_capnp::BlockField::ParentHash,
319            BlockField::Sha3Uncles => crate::hypersync_net_types_capnp::BlockField::Sha3Uncles,
320            BlockField::LogsBloom => crate::hypersync_net_types_capnp::BlockField::LogsBloom,
321            BlockField::TransactionsRoot => {
322                crate::hypersync_net_types_capnp::BlockField::TransactionsRoot
323            }
324            BlockField::StateRoot => crate::hypersync_net_types_capnp::BlockField::StateRoot,
325            BlockField::ReceiptsRoot => crate::hypersync_net_types_capnp::BlockField::ReceiptsRoot,
326            BlockField::Miner => crate::hypersync_net_types_capnp::BlockField::Miner,
327            BlockField::ExtraData => crate::hypersync_net_types_capnp::BlockField::ExtraData,
328            BlockField::Size => crate::hypersync_net_types_capnp::BlockField::Size,
329            BlockField::GasLimit => crate::hypersync_net_types_capnp::BlockField::GasLimit,
330            BlockField::GasUsed => crate::hypersync_net_types_capnp::BlockField::GasUsed,
331            BlockField::Timestamp => crate::hypersync_net_types_capnp::BlockField::Timestamp,
332            BlockField::MixHash => crate::hypersync_net_types_capnp::BlockField::MixHash,
333            BlockField::Nonce => crate::hypersync_net_types_capnp::BlockField::Nonce,
334            BlockField::Difficulty => crate::hypersync_net_types_capnp::BlockField::Difficulty,
335            BlockField::TotalDifficulty => {
336                crate::hypersync_net_types_capnp::BlockField::TotalDifficulty
337            }
338            BlockField::Uncles => crate::hypersync_net_types_capnp::BlockField::Uncles,
339            BlockField::BaseFeePerGas => {
340                crate::hypersync_net_types_capnp::BlockField::BaseFeePerGas
341            }
342            BlockField::BlobGasUsed => crate::hypersync_net_types_capnp::BlockField::BlobGasUsed,
343            BlockField::ExcessBlobGas => {
344                crate::hypersync_net_types_capnp::BlockField::ExcessBlobGas
345            }
346            BlockField::ParentBeaconBlockRoot => {
347                crate::hypersync_net_types_capnp::BlockField::ParentBeaconBlockRoot
348            }
349            BlockField::WithdrawalsRoot => {
350                crate::hypersync_net_types_capnp::BlockField::WithdrawalsRoot
351            }
352            BlockField::Withdrawals => crate::hypersync_net_types_capnp::BlockField::Withdrawals,
353            BlockField::L1BlockNumber => {
354                crate::hypersync_net_types_capnp::BlockField::L1BlockNumber
355            }
356            BlockField::SendCount => crate::hypersync_net_types_capnp::BlockField::SendCount,
357            BlockField::SendRoot => crate::hypersync_net_types_capnp::BlockField::SendRoot,
358        }
359    }
360
361    /// Convert Cap'n Proto enum to BlockField
362    pub fn from_capnp(field: crate::hypersync_net_types_capnp::BlockField) -> Self {
363        match field {
364            crate::hypersync_net_types_capnp::BlockField::Number => BlockField::Number,
365            crate::hypersync_net_types_capnp::BlockField::Hash => BlockField::Hash,
366            crate::hypersync_net_types_capnp::BlockField::ParentHash => BlockField::ParentHash,
367            crate::hypersync_net_types_capnp::BlockField::Sha3Uncles => BlockField::Sha3Uncles,
368            crate::hypersync_net_types_capnp::BlockField::LogsBloom => BlockField::LogsBloom,
369            crate::hypersync_net_types_capnp::BlockField::TransactionsRoot => {
370                BlockField::TransactionsRoot
371            }
372            crate::hypersync_net_types_capnp::BlockField::StateRoot => BlockField::StateRoot,
373            crate::hypersync_net_types_capnp::BlockField::ReceiptsRoot => BlockField::ReceiptsRoot,
374            crate::hypersync_net_types_capnp::BlockField::Miner => BlockField::Miner,
375            crate::hypersync_net_types_capnp::BlockField::ExtraData => BlockField::ExtraData,
376            crate::hypersync_net_types_capnp::BlockField::Size => BlockField::Size,
377            crate::hypersync_net_types_capnp::BlockField::GasLimit => BlockField::GasLimit,
378            crate::hypersync_net_types_capnp::BlockField::GasUsed => BlockField::GasUsed,
379            crate::hypersync_net_types_capnp::BlockField::Timestamp => BlockField::Timestamp,
380            crate::hypersync_net_types_capnp::BlockField::MixHash => BlockField::MixHash,
381            crate::hypersync_net_types_capnp::BlockField::Nonce => BlockField::Nonce,
382            crate::hypersync_net_types_capnp::BlockField::Difficulty => BlockField::Difficulty,
383            crate::hypersync_net_types_capnp::BlockField::TotalDifficulty => {
384                BlockField::TotalDifficulty
385            }
386            crate::hypersync_net_types_capnp::BlockField::Uncles => BlockField::Uncles,
387            crate::hypersync_net_types_capnp::BlockField::BaseFeePerGas => {
388                BlockField::BaseFeePerGas
389            }
390            crate::hypersync_net_types_capnp::BlockField::BlobGasUsed => BlockField::BlobGasUsed,
391            crate::hypersync_net_types_capnp::BlockField::ExcessBlobGas => {
392                BlockField::ExcessBlobGas
393            }
394            crate::hypersync_net_types_capnp::BlockField::ParentBeaconBlockRoot => {
395                BlockField::ParentBeaconBlockRoot
396            }
397            crate::hypersync_net_types_capnp::BlockField::WithdrawalsRoot => {
398                BlockField::WithdrawalsRoot
399            }
400            crate::hypersync_net_types_capnp::BlockField::Withdrawals => BlockField::Withdrawals,
401            crate::hypersync_net_types_capnp::BlockField::L1BlockNumber => {
402                BlockField::L1BlockNumber
403            }
404            crate::hypersync_net_types_capnp::BlockField::SendCount => BlockField::SendCount,
405            crate::hypersync_net_types_capnp::BlockField::SendRoot => BlockField::SendRoot,
406        }
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use std::str::FromStr;
413
414    use hypersync_format::Hex;
415
416    use super::*;
417    use crate::{query::tests::test_query_serde, Query};
418
419    #[test]
420    fn test_all_fields_in_schema() {
421        let schema = hypersync_schema::block_header();
422        let schema_fields = schema
423            .fields
424            .iter()
425            .map(|f| f.name.clone())
426            .collect::<BTreeSet<_>>();
427        let all_fields = BlockField::all()
428            .into_iter()
429            .map(|f| f.as_ref().to_string())
430            .collect::<BTreeSet<_>>();
431        assert_eq!(schema_fields, all_fields);
432    }
433
434    #[test]
435    fn test_serde_matches_strum() {
436        for field in BlockField::all() {
437            let serialized = serde_json::to_string(&field).unwrap();
438            let strum = serde_json::to_string(&field.as_ref()).unwrap();
439            assert_eq!(serialized, strum, "strum value should be the same as serde");
440        }
441    }
442
443    #[test]
444    fn test_block_filter_serde_with_values() {
445        let block_filter = BlockFilter {
446            hash: vec![Hash::decode_hex(
447                "0x40d008f2a1653f09b7b028d30c7fd1ba7c84900fcfb032040b3eb3d16f84d294",
448            )
449            .unwrap()],
450            miner: vec![Address::decode_hex("0xdadB0d80178819F2319190D340ce9A924f783711").unwrap()],
451        };
452        let query = Query::new()
453            .where_blocks(block_filter)
454            .select_block_fields(BlockField::all());
455
456        test_query_serde(query, "block selection with rest defaults");
457    }
458
459    #[test]
460    fn test_as_str() {
461        let block_field = BlockField::Number;
462        let from_str = BlockField::from_str("number").unwrap();
463        assert_eq!(block_field, from_str);
464    }
465}