tiders_rpc_client/
query.rs

1//! EVM query types for the RPC client.
2//!
3//! These mirror `tiders_ingest::evm::Query` but are owned by this crate
4//! so the RPC client can evolve independently.
5
6use anyhow::{bail, Result};
7use log::warn;
8
9/// A 20-byte Ethereum address.
10#[derive(Default, Debug, Clone, Copy)]
11pub struct Address(pub [u8; 20]);
12
13/// A 32-byte log topic value.
14#[derive(Default, Debug, Clone, Copy)]
15pub struct Topic(pub [u8; 32]);
16
17/// A 4-byte function selector (first 4 bytes of the keccak-256 hash of the function signature).
18#[derive(Default, Debug, Clone, Copy)]
19pub struct Sighash(pub [u8; 4]);
20
21/// A 32-byte hash (e.g. transaction hash, block hash).
22#[derive(Default, Debug, Clone, Copy)]
23pub struct Hash(pub [u8; 32]);
24
25/// Describes what data to fetch from the RPC provider.
26///
27/// A query specifies a block range, which EVM tables to populate (logs,
28/// transactions, traces), and which fields to include in each table.
29#[derive(Default, Debug, Clone)]
30pub struct Query {
31    /// First block to fetch (inclusive).
32    pub from_block: u64,
33    /// Last block to fetch (inclusive). `None` means stream up to the current head.
34    pub to_block: Option<u64>,
35    /// When `true`, fetch block headers even if no log/transaction/trace
36    /// request is present.
37    pub include_all_blocks: bool,
38    /// Log filter requests. Each entry produces separate `eth_getLogs` calls
39    /// and results are merged.
40    pub logs: Vec<LogRequest>,
41    /// Transaction requests. Presence triggers the block pipeline
42    /// (`eth_getBlockByNumber`).
43    pub transactions: Vec<TransactionRequest>,
44    /// Trace requests. Presence triggers the trace pipeline
45    /// (`trace_block` or `debug_traceBlockByNumber`).
46    pub traces: Vec<TraceRequest>,
47    /// Controls which columns appear in the output Arrow batches.
48    pub fields: Fields,
49}
50
51/// Filters for the `eth_getLogs` pipeline.
52///
53/// Multiple addresses and topics are OR'd together by the provider.
54/// Use `include_*` flags to request cross-pipeline data (blocks,
55/// transactions, traces) for the same block range.
56#[derive(Default, Debug, Clone)]
57pub struct LogRequest {
58    pub address: Vec<Address>,
59    pub topic0: Vec<Topic>,
60    pub topic1: Vec<Topic>,
61    pub topic2: Vec<Topic>,
62    pub topic3: Vec<Topic>,
63    /// Also fetch transactions for the same block range.
64    pub include_transactions: bool,
65    /// Ignored by the RPC client (log filters are not allowed in cross-pipeline queries).
66    pub include_transaction_logs: bool,
67    /// Also fetch traces for the same block range.
68    pub include_transaction_traces: bool,
69    /// Also fetch block headers for the same block range.
70    pub include_blocks: bool,
71}
72
73/// Request for the block pipeline (`eth_getBlockByNumber`).
74///
75/// Filter fields (`from_`, `to`, `sighash`, etc.) are **not supported** by the
76/// RPC client — the provider returns all transactions per block. Populate
77/// these only if using a tiders client that supports server-side filtering.
78#[derive(Default, Debug, Clone)]
79pub struct TransactionRequest {
80    pub from_: Vec<Address>,
81    pub to: Vec<Address>,
82    pub sighash: Vec<Sighash>,
83    pub status: Vec<u8>,
84    pub type_: Vec<u8>,
85    pub contract_deployment_address: Vec<Address>,
86    pub hash: Vec<Hash>,
87    /// Also fetch logs for the same block range.
88    pub include_logs: bool,
89    /// Also fetch traces for the same block range.
90    pub include_traces: bool,
91    /// Included for API compatibility; blocks are always fetched by this pipeline.
92    pub include_blocks: bool,
93}
94
95/// Request for the trace pipeline (`trace_block` / `debug_traceBlockByNumber`).
96///
97/// Filter fields (`from_`, `to`, `call_type`, etc.) are **not supported** by
98/// the RPC client — the provider returns all traces per block. Populate
99/// these only if using a tiders client that supports server-side filtering.
100#[derive(Default, Debug, Clone)]
101pub struct TraceRequest {
102    pub from_: Vec<Address>,
103    pub to: Vec<Address>,
104    pub address: Vec<Address>,
105    pub call_type: Vec<String>,
106    pub reward_type: Vec<String>,
107    pub type_: Vec<String>,
108    pub sighash: Vec<Sighash>,
109    pub author: Vec<Address>,
110    /// Which RPC method to use for fetching traces.
111    pub trace_method: TraceMethod,
112    /// Also fetch transactions for the same block range.
113    pub include_transactions: bool,
114    /// Also fetch logs for the same block range.
115    pub include_transaction_logs: bool,
116    /// Also fetch traces for the same block range (no-op since traces are already fetched).
117    pub include_transaction_traces: bool,
118    /// Also fetch block headers for the same block range.
119    pub include_blocks: bool,
120}
121
122/// Which RPC method to use for fetching execution traces.
123#[derive(Debug, Clone, Copy, Default)]
124pub enum TraceMethod {
125    /// Parity-style `trace_block` (Erigon, Nethermind, Reth).
126    #[default]
127    TraceBlock,
128    /// Geth-style `debug_traceBlockByNumber` with `callTracer`.
129    DebugTraceBlockByNumber,
130}
131
132/// Column selection for each EVM table in the output.
133#[derive(Default, Debug, Clone, Copy)]
134pub struct Fields {
135    pub block: BlockFields,
136    pub transaction: TransactionFields,
137    pub log: LogFields,
138    pub trace: TraceFields,
139}
140
141/// Boolean flags selecting which block columns to include in the output.
142///
143/// When all flags are `false` (the default), the full schema is returned.
144#[derive(Default, Debug, Clone, Copy)]
145pub struct BlockFields {
146    pub number: bool,
147    pub hash: bool,
148    pub parent_hash: bool,
149    pub nonce: bool,
150    pub sha3_uncles: bool,
151    pub logs_bloom: bool,
152    pub transactions_root: bool,
153    pub state_root: bool,
154    pub receipts_root: bool,
155    pub miner: bool,
156    pub difficulty: bool,
157    pub total_difficulty: bool,
158    pub extra_data: bool,
159    pub size: bool,
160    pub gas_limit: bool,
161    pub gas_used: bool,
162    pub timestamp: bool,
163    pub uncles: bool,
164    pub base_fee_per_gas: bool,
165    pub blob_gas_used: bool,
166    pub excess_blob_gas: bool,
167    pub parent_beacon_block_root: bool,
168    pub withdrawals_root: bool,
169    pub withdrawals: bool,
170    pub l1_block_number: bool,
171    pub send_count: bool,
172    pub send_root: bool,
173    pub mix_hash: bool,
174}
175
176/// Boolean flags selecting which transaction columns to include in the output.
177///
178/// When all flags are `false` (the default), the full schema is returned.
179#[derive(Default, Debug, Clone, Copy)]
180pub struct TransactionFields {
181    pub block_hash: bool,
182    pub block_number: bool,
183    pub from: bool,
184    pub gas: bool,
185    pub gas_price: bool,
186    pub hash: bool,
187    pub input: bool,
188    pub nonce: bool,
189    pub to: bool,
190    pub transaction_index: bool,
191    pub value: bool,
192    pub v: bool,
193    pub r: bool,
194    pub s: bool,
195    pub max_priority_fee_per_gas: bool,
196    pub max_fee_per_gas: bool,
197    pub chain_id: bool,
198    pub cumulative_gas_used: bool,
199    pub effective_gas_price: bool,
200    pub gas_used: bool,
201    pub contract_address: bool,
202    pub logs_bloom: bool,
203    pub type_: bool,
204    pub root: bool,
205    pub status: bool,
206    pub sighash: bool,
207    pub y_parity: bool,
208    pub access_list: bool,
209    pub l1_fee: bool,
210    pub l1_gas_price: bool,
211    pub l1_fee_scalar: bool,
212    pub gas_used_for_l1: bool,
213    pub max_fee_per_blob_gas: bool,
214    pub blob_versioned_hashes: bool,
215    pub deposit_nonce: bool,
216    pub blob_gas_price: bool,
217    pub deposit_receipt_version: bool,
218    pub blob_gas_used: bool,
219    pub l1_base_fee_scalar: bool,
220    pub l1_blob_base_fee: bool,
221    pub l1_blob_base_fee_scalar: bool,
222    pub l1_block_number: bool,
223    pub mint: bool,
224    pub source_hash: bool,
225}
226
227/// Boolean flags selecting which log columns to include in the output.
228///
229/// When all flags are `false` (the default), the full schema is returned.
230#[derive(Default, Debug, Clone, Copy)]
231pub struct LogFields {
232    pub removed: bool,
233    pub log_index: bool,
234    pub transaction_index: bool,
235    pub transaction_hash: bool,
236    pub block_hash: bool,
237    pub block_number: bool,
238    pub address: bool,
239    pub data: bool,
240    pub topic0: bool,
241    pub topic1: bool,
242    pub topic2: bool,
243    pub topic3: bool,
244}
245
246/// Boolean flags selecting which trace columns to include in the output.
247///
248/// When all flags are `false` (the default), the full schema is returned.
249#[derive(Default, Debug, Clone, Copy)]
250pub struct TraceFields {
251    pub from: bool,
252    pub to: bool,
253    pub call_type: bool,
254    pub gas: bool,
255    pub input: bool,
256    pub init: bool,
257    pub value: bool,
258    pub author: bool,
259    pub reward_type: bool,
260    pub block_hash: bool,
261    pub block_number: bool,
262    pub address: bool,
263    pub code: bool,
264    pub gas_used: bool,
265    pub output: bool,
266    pub subtraces: bool,
267    pub trace_address: bool,
268    pub transaction_hash: bool,
269    pub transaction_position: bool,
270    pub type_: bool,
271    pub error: bool,
272    pub sighash: bool,
273    pub action_address: bool,
274    pub balance: bool,
275    pub refund_address: bool,
276}
277
278/// Return `true` if any of the listed bool fields is set.
279macro_rules! any_field_set {
280    ($obj:expr, $( $field:ident ),+ $(,)?) => {
281        $( $obj.$field )||+
282    };
283}
284
285/// Describes which pipelines should be run for a query.
286///
287/// The three pipelines correspond to distinct RPC methods:
288/// - `blocks_transactions`: `eth_getBlockByNumber` — fetches blocks and transactions.
289/// - `logs`: `eth_getLogs` — fetches event logs.
290/// - `traces`: `trace_block` or `debug_traceBlockByNumber` — fetches traces.
291///
292/// When more than one flag is set, the coordinator runs all pipelines over the
293/// same block range and merges results into a single `ArrowResponse`.
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295pub(crate) struct Pipelines {
296    pub blocks_transactions: bool,
297    pub traces: bool,
298    pub logs: bool,
299}
300
301impl Pipelines {
302    /// Returns `true` if more than one pipeline is needed, requiring coordination.
303    pub fn needs_coordinator(self) -> bool {
304        (u8::from(self.blocks_transactions) + u8::from(self.logs) + u8::from(self.traces)) > 1
305    }
306}
307
308/// Validate a query and determine which RPC pipelines it requires.
309///
310/// Returns an error if the query:
311/// - Selects fields that belong to different RPC pipelines (e.g. log fields
312///   together with block/transaction fields) **without** using `include_*` flags.
313/// - Has a `LogRequest` with both `include_*` flags and non-empty address/topic
314///   filters. Cross pipeline queries are only supported when the result is the full
315///   block range with no client-side filtering.
316/// - Includes any filter fields on a `TransactionRequest` or `TraceRequest`.
317///   RPC has no server-side filtering for blocks, transactions, or traces: every
318///   block in the requested range is fetched unconditionally. Filtering must be
319///   done by the caller after data is ingested (post-indexing).
320pub(crate) fn analyze_query(query: &Query) -> Result<Pipelines> {
321    let has_log_fields = has_any_log_field(&query.fields.log);
322    let has_block_fields = has_any_block_field(&query.fields.block);
323    let has_tx_fields = has_any_transaction_field(&query.fields.transaction);
324    let has_trace_fields = has_any_trace_field(&query.fields.trace);
325
326    let uses_log_pipeline = !query.logs.is_empty() || has_log_fields;
327    let uses_block_pipeline = !query.transactions.is_empty()
328        || has_block_fields
329        || has_tx_fields
330        || query.include_all_blocks;
331    let uses_trace_pipeline = !query.traces.is_empty() || has_trace_fields;
332
333    // Collect cross-pipeline requests from include_* flags.
334    let mut cross_blocks_transactions = false;
335    let mut cross_logs = false;
336    let mut cross_traces = false;
337
338    for req in &query.logs {
339        // include_transactions and include_blocks request the Block pipeline.
340        cross_blocks_transactions |= req.include_transactions || req.include_blocks;
341        cross_traces |= req.include_transaction_traces;
342        if req.include_transaction_logs {
343            warn!("Logs request include_transaction_logs=true is ineffective since logs filters are not allowed in RPC client cross pipeline queries.");
344        }
345    }
346
347    for req in &query.transactions {
348        cross_logs |= req.include_logs;
349        cross_traces |= req.include_traces;
350        // Note: include_blocks is NOT checked here because transactions
351        // already come from the Block pipeline (eth_getBlockByNumber),
352        // which inherently fetches both blocks and transactions.
353    }
354
355    for req in &query.traces {
356        // include_transactions and include_blocks request the Block pipeline.
357        cross_blocks_transactions |= req.include_transactions || req.include_blocks;
358        cross_logs |= req.include_transaction_logs;
359    }
360
361    let has_cross_pipeline = cross_blocks_transactions || cross_logs || cross_traces;
362
363    // 1. Cross-pipeline field selection.
364    //
365    // Mixing pipelines via field selectors is only allowed when the user has
366    // opted in via `include_*` flags. Without those flags, a query that
367    // touches multiple pipelines is an error.
368    let pipeline_count =
369        u8::from(uses_log_pipeline) + u8::from(uses_block_pipeline) + u8::from(uses_trace_pipeline);
370    if pipeline_count > 1 && !has_cross_pipeline {
371        let mut pipelines = Vec::new();
372        if uses_log_pipeline {
373            pipelines.push("eth_getLogs (log fields/filters)");
374        }
375        if uses_block_pipeline {
376            pipelines.push("eth_getBlockByNumber (block/transaction fields/filters)");
377        }
378        if uses_trace_pipeline {
379            pipelines.push("trace_block (trace fields/filters)");
380        }
381        bail!(
382            "Query mixes fields/filters from different RPC pipelines: [{}]. \
383             Use include_* flags on your request types to enable cross-pipeline \
384             coordination, or split your indexer into separate pipelines and \
385             filter/join post-indexing.",
386            pipelines.join(", ")
387        );
388    }
389
390    // 2. LogRequest include_* flags conflict with address/topic filters.
391    //
392    // When include_* is set on a LogRequest, the secondary pipelines
393    // (eth_getBlockByNumber, trace_block) return all data for the full block
394    // range with no server-side filtering. Combining that with log-side filters
395    // would make the log table a filtered subset while blocks/transactions/traces
396    // are unfiltered — an inconsistency the RPC layer cannot resolve.
397    if has_cross_pipeline {
398        for (i, req) in query.logs.iter().enumerate() {
399            let has_include = req.include_transactions
400                || req.include_transaction_logs
401                || req.include_transaction_traces
402                || req.include_blocks;
403            if has_include {
404                let mut filters: Vec<&str> = Vec::new();
405                if !req.address.is_empty() {
406                    filters.push("address");
407                }
408                if !req.topic0.is_empty() {
409                    filters.push("topic0");
410                }
411                if !req.topic1.is_empty() {
412                    filters.push("topic1");
413                }
414                if !req.topic2.is_empty() {
415                    filters.push("topic2");
416                }
417                if !req.topic3.is_empty() {
418                    filters.push("topic3");
419                }
420                if !filters.is_empty() {
421                    bail!(
422                        "logs[{i}] sets both include_* flags ({}) and field filters [{}]. \
423                         Cross pipelines queries can only be used when the result is the full block range with no client-side filtering. \
424                         Remove the log filters to use cross-pipeline coordination.",
425                        [
426                            req.include_transactions.then_some("include_transactions"),
427                            req.include_transaction_logs.then_some("include_transaction_logs"),
428                            req.include_transaction_traces.then_some("include_transaction_traces"),
429                            req.include_blocks.then_some("include_blocks"),
430                        ]
431                        .into_iter()
432                        .flatten()
433                        .collect::<Vec<_>>()
434                        .join(", "),
435                        filters.join(", ")
436                    );
437                }
438            }
439        }
440    }
441
442    // 3. TransactionRequest filter fields are not supported.
443    //
444    // eth_getBlockByNumber returns every transaction in a block with no
445    // server-side filtering. Applying filters inside the client would fetch
446    // all blocks anyway and silently hide that cost. Instead, ingest all
447    // transactions and filter in your database post-indexing.
448    if !query.transactions.is_empty() {
449        for (i, req) in query.transactions.iter().enumerate() {
450            let mut unsupported: Vec<&str> = Vec::new();
451            if !req.from_.is_empty() {
452                unsupported.push("from_");
453            }
454            if !req.to.is_empty() {
455                unsupported.push("to");
456            }
457            if !req.sighash.is_empty() {
458                unsupported.push("sighash");
459            }
460            if !req.type_.is_empty() {
461                unsupported.push("type_");
462            }
463            if !req.hash.is_empty() {
464                unsupported.push("hash");
465            }
466            if !req.status.is_empty() {
467                unsupported.push("status");
468            }
469            if !req.contract_deployment_address.is_empty() {
470                unsupported.push("contract_deployment_address");
471            }
472            if !unsupported.is_empty() {
473                bail!(
474                    "transactions[{i}] sets filter fields [{}] which are not supported by the \
475                     RPC block pipeline. eth_getBlockByNumber returns all transactions in a block \
476                     with no server-side filtering — every block in the range is fetched \
477                     regardless. Remove the filters and perform them post-indexing in your \
478                     database instead or use a different tiders client that supports filtering data on source.",
479                    unsupported.join(", ")
480                );
481            }
482        }
483    }
484
485    // 4. TraceRequest filter fields are not supported.
486    //
487    // trace_block / debug_traceBlockByNumber returns every trace in a block with
488    // no server-side filtering. Applying filters inside the client would fetch
489    // all traces anyway and silently hide that cost. Instead, ingest all traces
490    // and filter in your database post-indexing.
491    if !query.traces.is_empty() {
492        for (i, req) in query.traces.iter().enumerate() {
493            let mut unsupported: Vec<&str> = Vec::new();
494            if !req.from_.is_empty() {
495                unsupported.push("from_");
496            }
497            if !req.to.is_empty() {
498                unsupported.push("to");
499            }
500            if !req.address.is_empty() {
501                unsupported.push("address");
502            }
503            if !req.call_type.is_empty() {
504                unsupported.push("call_type");
505            }
506            if !req.reward_type.is_empty() {
507                unsupported.push("reward_type");
508            }
509            if !req.type_.is_empty() {
510                unsupported.push("type_");
511            }
512            if !req.sighash.is_empty() {
513                unsupported.push("sighash");
514            }
515            if !req.author.is_empty() {
516                unsupported.push("author");
517            }
518            if !unsupported.is_empty() {
519                bail!(
520                    "traces[{i}] sets filter fields [{}] which are not supported by the \
521                     RPC trace pipeline. trace_block / debug_traceBlockByNumber returns all \
522                     traces in a block with no server-side filtering — every block in the range \
523                     is fetched regardless. Remove the filters and perform them post-indexing in \
524                     your database instead or use a different tiders client that supports \
525                     filtering data on source.",
526                    unsupported.join(", ")
527                );
528            }
529        }
530    }
531
532    // 5. Build the final Pipelines, combining direct query usage with
533    //    cross-pipeline include_* requests.
534    Ok(Pipelines {
535        blocks_transactions: uses_block_pipeline || cross_blocks_transactions,
536        logs: uses_log_pipeline || cross_logs,
537        traces: uses_trace_pipeline || cross_traces,
538    })
539}
540
541/// Return the `TraceMethod` to use for the query.
542///
543/// Uses the first `TraceRequest`'s method, defaulting to `TraceBlock`.
544pub(crate) fn get_trace_method(query: &Query) -> TraceMethod {
545    query
546        .traces
547        .first()
548        .map(|r| r.trace_method)
549        .unwrap_or_default()
550}
551
552fn has_any_log_field(f: &LogFields) -> bool {
553    any_field_set!(
554        f,
555        removed,
556        log_index,
557        transaction_index,
558        transaction_hash,
559        block_hash,
560        block_number,
561        address,
562        data,
563        topic0,
564        topic1,
565        topic2,
566        topic3,
567    )
568}
569
570fn has_any_block_field(f: &BlockFields) -> bool {
571    any_field_set!(
572        f,
573        number,
574        hash,
575        parent_hash,
576        nonce,
577        sha3_uncles,
578        logs_bloom,
579        transactions_root,
580        state_root,
581        receipts_root,
582        miner,
583        difficulty,
584        total_difficulty,
585        extra_data,
586        size,
587        gas_limit,
588        gas_used,
589        timestamp,
590        uncles,
591        base_fee_per_gas,
592        blob_gas_used,
593        excess_blob_gas,
594        parent_beacon_block_root,
595        withdrawals_root,
596        withdrawals,
597        l1_block_number,
598        send_count,
599        send_root,
600        mix_hash,
601    )
602}
603
604fn has_any_transaction_field(f: &TransactionFields) -> bool {
605    any_field_set!(
606        f,
607        block_hash,
608        block_number,
609        from,
610        gas,
611        gas_price,
612        hash,
613        input,
614        nonce,
615        to,
616        transaction_index,
617        value,
618        v,
619        r,
620        s,
621        max_priority_fee_per_gas,
622        max_fee_per_gas,
623        chain_id,
624        cumulative_gas_used,
625        effective_gas_price,
626        gas_used,
627        contract_address,
628        logs_bloom,
629        type_,
630        root,
631        status,
632        sighash,
633        y_parity,
634        access_list,
635        l1_fee,
636        l1_gas_price,
637        l1_fee_scalar,
638        gas_used_for_l1,
639        max_fee_per_blob_gas,
640        blob_versioned_hashes,
641        deposit_nonce,
642        blob_gas_price,
643        deposit_receipt_version,
644        blob_gas_used,
645        l1_base_fee_scalar,
646        l1_blob_base_fee,
647        l1_blob_base_fee_scalar,
648        l1_block_number,
649        mint,
650        source_hash,
651    )
652}
653
654fn has_any_tx_receipt_field(f: &TransactionFields) -> bool {
655    any_field_set!(
656        f,
657        cumulative_gas_used,
658        effective_gas_price,
659        gas_used,
660        contract_address,
661        logs_bloom,
662        root,
663        status,
664    )
665}
666
667fn has_any_trace_field(f: &TraceFields) -> bool {
668    any_field_set!(
669        f,
670        from,
671        to,
672        call_type,
673        gas,
674        input,
675        init,
676        value,
677        author,
678        reward_type,
679        block_hash,
680        block_number,
681        address,
682        code,
683        gas_used,
684        output,
685        subtraces,
686        trace_address,
687        transaction_hash,
688        transaction_position,
689        type_,
690        error,
691        sighash,
692        action_address,
693        balance,
694        refund_address,
695    )
696}
697
698/// Return `true` if any transaction field *other than `hash`* is set.
699///
700/// Used to determine whether `eth_getBlockByNumber` must be called with `include_txs=true`.
701fn has_tx_fields_except_hash(f: &TransactionFields) -> bool {
702    any_field_set!(
703        f,
704        block_hash,
705        block_number,
706        from,
707        gas,
708        gas_price,
709        input,
710        nonce,
711        to,
712        transaction_index,
713        value,
714        v,
715        r,
716        s,
717        max_priority_fee_per_gas,
718        max_fee_per_gas,
719        chain_id,
720        cumulative_gas_used,
721        effective_gas_price,
722        gas_used,
723        contract_address,
724        logs_bloom,
725        type_,
726        root,
727        status,
728        sighash,
729        y_parity,
730        access_list,
731        l1_fee,
732        l1_gas_price,
733        l1_fee_scalar,
734        gas_used_for_l1,
735        max_fee_per_blob_gas,
736        blob_versioned_hashes,
737        deposit_nonce,
738        blob_gas_price,
739        deposit_receipt_version,
740        blob_gas_used,
741        l1_base_fee_scalar,
742        l1_blob_base_fee,
743        l1_blob_base_fee_scalar,
744        l1_block_number,
745        mint,
746        source_hash,
747    )
748}
749
750/// Return `true` if `eth_getBlockByNumber` must be called with `include_txs=true`.
751///
752/// Full transaction objects are needed when any transaction field other than
753/// `hash` is requested.
754pub(crate) fn get_blocks_needs_full_txs(query: &Query) -> bool {
755    has_tx_fields_except_hash(&query.fields.transaction)
756}
757
758/// Return `true` if the query requests any field that only comes from receipt data.
759///
760/// Used by the block pipeline to decide whether to call `eth_getBlockReceipts`.
761pub(crate) fn needs_tx_receipts(query: &Query) -> bool {
762    has_any_tx_receipt_field(&query.fields.transaction)
763}