1use anyhow::{bail, Result};
7use log::warn;
8
9#[derive(Default, Debug, Clone, Copy)]
11pub struct Address(pub [u8; 20]);
12
13#[derive(Default, Debug, Clone, Copy)]
15pub struct Topic(pub [u8; 32]);
16
17#[derive(Default, Debug, Clone, Copy)]
19pub struct Sighash(pub [u8; 4]);
20
21#[derive(Default, Debug, Clone, Copy)]
23pub struct Hash(pub [u8; 32]);
24
25#[derive(Default, Debug, Clone)]
30pub struct Query {
31 pub from_block: u64,
33 pub to_block: Option<u64>,
35 pub include_all_blocks: bool,
38 pub logs: Vec<LogRequest>,
41 pub transactions: Vec<TransactionRequest>,
44 pub traces: Vec<TraceRequest>,
47 pub fields: Fields,
49}
50
51#[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 pub include_transactions: bool,
65 pub include_transaction_logs: bool,
67 pub include_transaction_traces: bool,
69 pub include_blocks: bool,
71}
72
73#[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 pub include_logs: bool,
89 pub include_traces: bool,
91 pub include_blocks: bool,
93}
94
95#[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 pub trace_method: TraceMethod,
112 pub include_transactions: bool,
114 pub include_transaction_logs: bool,
116 pub include_transaction_traces: bool,
118 pub include_blocks: bool,
120}
121
122#[derive(Debug, Clone, Copy, Default)]
124pub enum TraceMethod {
125 #[default]
127 TraceBlock,
128 DebugTraceBlockByNumber,
130}
131
132#[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#[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#[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#[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#[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
278macro_rules! any_field_set {
280 ($obj:expr, $( $field:ident ),+ $(,)?) => {
281 $( $obj.$field )||+
282 };
283}
284
285#[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 pub fn needs_coordinator(self) -> bool {
304 (u8::from(self.blocks_transactions) + u8::from(self.logs) + u8::from(self.traces)) > 1
305 }
306}
307
308pub(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 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 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 }
354
355 for req in &query.traces {
356 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 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 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 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 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 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
541pub(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
698fn 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
750pub(crate) fn get_blocks_needs_full_txs(query: &Query) -> bool {
755 has_tx_fields_except_hash(&query.fields.transaction)
756}
757
758pub(crate) fn needs_tx_receipts(query: &Query) -> bool {
762 has_any_tx_receipt_field(&query.fields.transaction)
763}