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