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    /// Convert BlockField to Cap'n Proto enum
316    pub fn to_capnp(&self) -> crate::hypersync_net_types_capnp::BlockField {
317        match self {
318            BlockField::Number => crate::hypersync_net_types_capnp::BlockField::Number,
319            BlockField::Hash => crate::hypersync_net_types_capnp::BlockField::Hash,
320            BlockField::ParentHash => crate::hypersync_net_types_capnp::BlockField::ParentHash,
321            BlockField::Sha3Uncles => crate::hypersync_net_types_capnp::BlockField::Sha3Uncles,
322            BlockField::LogsBloom => crate::hypersync_net_types_capnp::BlockField::LogsBloom,
323            BlockField::TransactionsRoot => {
324                crate::hypersync_net_types_capnp::BlockField::TransactionsRoot
325            }
326            BlockField::StateRoot => crate::hypersync_net_types_capnp::BlockField::StateRoot,
327            BlockField::ReceiptsRoot => crate::hypersync_net_types_capnp::BlockField::ReceiptsRoot,
328            BlockField::Miner => crate::hypersync_net_types_capnp::BlockField::Miner,
329            BlockField::ExtraData => crate::hypersync_net_types_capnp::BlockField::ExtraData,
330            BlockField::Size => crate::hypersync_net_types_capnp::BlockField::Size,
331            BlockField::GasLimit => crate::hypersync_net_types_capnp::BlockField::GasLimit,
332            BlockField::GasUsed => crate::hypersync_net_types_capnp::BlockField::GasUsed,
333            BlockField::Timestamp => crate::hypersync_net_types_capnp::BlockField::Timestamp,
334            BlockField::MixHash => crate::hypersync_net_types_capnp::BlockField::MixHash,
335            BlockField::Nonce => crate::hypersync_net_types_capnp::BlockField::Nonce,
336            BlockField::Difficulty => crate::hypersync_net_types_capnp::BlockField::Difficulty,
337            BlockField::TotalDifficulty => {
338                crate::hypersync_net_types_capnp::BlockField::TotalDifficulty
339            }
340            BlockField::Uncles => crate::hypersync_net_types_capnp::BlockField::Uncles,
341            BlockField::BaseFeePerGas => {
342                crate::hypersync_net_types_capnp::BlockField::BaseFeePerGas
343            }
344            BlockField::BlobGasUsed => crate::hypersync_net_types_capnp::BlockField::BlobGasUsed,
345            BlockField::ExcessBlobGas => {
346                crate::hypersync_net_types_capnp::BlockField::ExcessBlobGas
347            }
348            BlockField::ParentBeaconBlockRoot => {
349                crate::hypersync_net_types_capnp::BlockField::ParentBeaconBlockRoot
350            }
351            BlockField::WithdrawalsRoot => {
352                crate::hypersync_net_types_capnp::BlockField::WithdrawalsRoot
353            }
354            BlockField::Withdrawals => crate::hypersync_net_types_capnp::BlockField::Withdrawals,
355            BlockField::L1BlockNumber => {
356                crate::hypersync_net_types_capnp::BlockField::L1BlockNumber
357            }
358            BlockField::SendCount => crate::hypersync_net_types_capnp::BlockField::SendCount,
359            BlockField::SendRoot => crate::hypersync_net_types_capnp::BlockField::SendRoot,
360        }
361    }
362
363    /// Convert Cap'n Proto enum to BlockField
364    pub fn from_capnp(field: crate::hypersync_net_types_capnp::BlockField) -> Self {
365        match field {
366            crate::hypersync_net_types_capnp::BlockField::Number => BlockField::Number,
367            crate::hypersync_net_types_capnp::BlockField::Hash => BlockField::Hash,
368            crate::hypersync_net_types_capnp::BlockField::ParentHash => BlockField::ParentHash,
369            crate::hypersync_net_types_capnp::BlockField::Sha3Uncles => BlockField::Sha3Uncles,
370            crate::hypersync_net_types_capnp::BlockField::LogsBloom => BlockField::LogsBloom,
371            crate::hypersync_net_types_capnp::BlockField::TransactionsRoot => {
372                BlockField::TransactionsRoot
373            }
374            crate::hypersync_net_types_capnp::BlockField::StateRoot => BlockField::StateRoot,
375            crate::hypersync_net_types_capnp::BlockField::ReceiptsRoot => BlockField::ReceiptsRoot,
376            crate::hypersync_net_types_capnp::BlockField::Miner => BlockField::Miner,
377            crate::hypersync_net_types_capnp::BlockField::ExtraData => BlockField::ExtraData,
378            crate::hypersync_net_types_capnp::BlockField::Size => BlockField::Size,
379            crate::hypersync_net_types_capnp::BlockField::GasLimit => BlockField::GasLimit,
380            crate::hypersync_net_types_capnp::BlockField::GasUsed => BlockField::GasUsed,
381            crate::hypersync_net_types_capnp::BlockField::Timestamp => BlockField::Timestamp,
382            crate::hypersync_net_types_capnp::BlockField::MixHash => BlockField::MixHash,
383            crate::hypersync_net_types_capnp::BlockField::Nonce => BlockField::Nonce,
384            crate::hypersync_net_types_capnp::BlockField::Difficulty => BlockField::Difficulty,
385            crate::hypersync_net_types_capnp::BlockField::TotalDifficulty => {
386                BlockField::TotalDifficulty
387            }
388            crate::hypersync_net_types_capnp::BlockField::Uncles => BlockField::Uncles,
389            crate::hypersync_net_types_capnp::BlockField::BaseFeePerGas => {
390                BlockField::BaseFeePerGas
391            }
392            crate::hypersync_net_types_capnp::BlockField::BlobGasUsed => BlockField::BlobGasUsed,
393            crate::hypersync_net_types_capnp::BlockField::ExcessBlobGas => {
394                BlockField::ExcessBlobGas
395            }
396            crate::hypersync_net_types_capnp::BlockField::ParentBeaconBlockRoot => {
397                BlockField::ParentBeaconBlockRoot
398            }
399            crate::hypersync_net_types_capnp::BlockField::WithdrawalsRoot => {
400                BlockField::WithdrawalsRoot
401            }
402            crate::hypersync_net_types_capnp::BlockField::Withdrawals => BlockField::Withdrawals,
403            crate::hypersync_net_types_capnp::BlockField::L1BlockNumber => {
404                BlockField::L1BlockNumber
405            }
406            crate::hypersync_net_types_capnp::BlockField::SendCount => BlockField::SendCount,
407            crate::hypersync_net_types_capnp::BlockField::SendRoot => BlockField::SendRoot,
408        }
409    }
410}
411
412#[cfg(test)]
413mod tests {
414    use std::str::FromStr;
415
416    use hypersync_format::Hex;
417
418    use super::*;
419    use crate::{query::tests::test_query_serde, Query};
420
421    #[test]
422    fn test_all_fields_in_schema() {
423        let schema = hypersync_schema::block_header();
424        let schema_fields = schema
425            .fields
426            .iter()
427            .map(|f| f.name.clone())
428            .collect::<BTreeSet<_>>();
429        let all_fields = BlockField::all()
430            .into_iter()
431            .map(|f| f.as_ref().to_string())
432            .collect::<BTreeSet<_>>();
433        assert_eq!(schema_fields, all_fields);
434    }
435
436    #[test]
437    fn test_serde_matches_strum() {
438        for field in BlockField::all() {
439            let serialized = serde_json::to_string(&field).unwrap();
440            let strum = serde_json::to_string(&field.as_ref()).unwrap();
441            assert_eq!(serialized, strum, "strum value should be the same as serde");
442        }
443    }
444
445    #[test]
446    fn test_block_filter_serde_with_values() {
447        let block_filter = BlockFilter {
448            hash: vec![Hash::decode_hex(
449                "0x40d008f2a1653f09b7b028d30c7fd1ba7c84900fcfb032040b3eb3d16f84d294",
450            )
451            .unwrap()],
452            miner: vec![Address::decode_hex("0xdadB0d80178819F2319190D340ce9A924f783711").unwrap()],
453        };
454        let query = Query::new()
455            .where_blocks(block_filter)
456            .select_block_fields(BlockField::all());
457
458        test_query_serde(query, "block selection with rest defaults");
459    }
460
461    #[test]
462    fn test_as_str() {
463        let block_field = BlockField::Number;
464        let from_str = BlockField::from_str("number").unwrap();
465        assert_eq!(block_field, from_str);
466    }
467}