datafusion_ethers/
stream.rs1use alloy_network::AnyNetwork;
2use alloy_provider::{DynProvider, Provider};
3use alloy_rpc_types_eth::{BlockNumberOrTag, Filter, FilterBlockOption, Log};
4use alloy_transport::{RpcError, TransportErrorKind};
5
6#[derive(Debug, Clone)]
9pub struct StreamOptions {
10 pub block_stride: u64,
13
14 pub use_block_timestamp_fallback: bool,
22}
23
24impl Default for StreamOptions {
25 fn default() -> Self {
26 Self {
27 block_stride: 10_000,
28 use_block_timestamp_fallback: false,
29 }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct StreamState {
37 pub last_seen_block: u64,
38}
39
40pub struct StreamBatch {
43 pub logs: Vec<Log>,
44 pub state: StreamState,
45 pub block_range_all: (u64, u64),
46}
47
48pub struct RawLogsStream;
51
52impl RawLogsStream {
53 pub fn paginate(
56 rpc_client: DynProvider<AnyNetwork>,
57 mut filter: Filter,
58 options: StreamOptions,
59 resume_from_state: Option<StreamState>,
60 ) -> impl futures::Stream<Item = Result<StreamBatch, RpcError<TransportErrorKind>>> {
61 async_stream::try_stream! {
62 let block_range_all = Self::filter_to_block_range(&rpc_client, &filter.block_option).await?;
65
66 let block_range_unprocessed = if let Some(last_seen_block) = resume_from_state.map(|s| s.last_seen_block) {
68 (
69 u64::max(last_seen_block + 1, block_range_all.0),
70 block_range_all.1,
71 )
72 } else {
73 block_range_all
74 };
75
76 tracing::info!(
77 block_range_query = ?filter.block_option,
78 ?block_range_all,
79 ?block_range_unprocessed,
80 "Computed block ranges",
81 );
82
83 let mut block_range_to_scan = block_range_unprocessed;
84
85 while block_range_to_scan.0 <= block_range_to_scan.1 {
86 let block_range_page = (
87 block_range_to_scan.0,
88 u64::min(
89 block_range_to_scan.1,
90 block_range_to_scan.0 + options.block_stride - 1,
91 ),
92 );
93
94 filter.block_option = FilterBlockOption::Range {
96 from_block: Some(block_range_page.0.into()),
97 to_block: Some(block_range_page.1.into()),
98 };
99
100 tracing::debug!(
101 ?block_range_page,
102 "Querying block range",
103 );
104
105 let mut logs = rpc_client.get_logs(&filter).await?;
107
108 if options.use_block_timestamp_fallback && logs.iter().any(|l| l.block_timestamp.is_none()) {
110 Self::populate_block_timestamps_fallback(&rpc_client, &mut logs).await?;
111 }
112
113 yield StreamBatch {
114 logs,
115 state: StreamState { last_seen_block: block_range_page.1 },
116 block_range_all,
117 };
118
119 block_range_to_scan.0 = block_range_page.1 + 1;
121 }
122 }
123 }
124
125 async fn populate_block_timestamps_fallback(
129 rpc_client: &DynProvider<AnyNetwork>,
130 logs: &mut [Log],
131 ) -> Result<(), RpcError<TransportErrorKind>> {
132 let first_log = logs.first().unwrap();
133 let last_log = logs.last().unwrap();
134
135 let first_block = rpc_client
136 .get_block_by_hash(first_log.block_hash.unwrap())
137 .await?
138 .unwrap();
139
140 let last_block = if first_log.block_hash == last_log.block_hash {
141 first_block.clone()
142 } else {
143 rpc_client
144 .get_block_by_hash(last_log.block_hash.unwrap())
145 .await?
146 .unwrap()
147 };
148
149 let time_per_block = ((last_block.header.timestamp - first_block.header.timestamp) as f64)
150 / ((last_block.number() - first_block.number()) as f64);
151
152 tracing::debug!(
153 block_range = ?(first_block.number(), last_block.number()),
154 time_per_block,
155 "Populating block timestamps via fallback mechanism",
156 );
157
158 for log in logs.iter_mut() {
159 let block_offset = log.block_number.unwrap() - first_block.number();
160 let time_offset = time_per_block * (block_offset as f64);
161 let ts = first_block.header.timestamp + (time_offset.floor() as u64);
162 log.block_timestamp = Some(ts);
163 }
164
165 Ok(())
166 }
167
168 pub async fn filter_to_block_range(
169 rpc_client: &DynProvider<AnyNetwork>,
170 block_option: &FilterBlockOption,
171 ) -> Result<(u64, u64), RpcError<TransportErrorKind>> {
172 match block_option {
173 FilterBlockOption::Range {
174 from_block: Some(from),
175 to_block: Some(to),
176 } => {
177 let from = match from {
178 BlockNumberOrTag::Earliest => 0,
179 BlockNumberOrTag::Number(n) => *n,
180 _ => Err(RpcError::local_usage_str(&format!(
181 "Invalid range: {block_option:?}"
182 )))?,
183 };
184 let to = match to {
185 BlockNumberOrTag::Number(n) => *n,
186 BlockNumberOrTag::Latest
187 | BlockNumberOrTag::Safe
188 | BlockNumberOrTag::Finalized => {
189 let Some(to_block) = rpc_client.get_block((*to).into()).await? else {
190 Err(RpcError::local_usage_str(&format!(
191 "Unable to resolve block: {to:?}"
192 )))?
193 };
194 to_block.header.number
195 }
196 _ => Err(RpcError::local_usage_str(&format!(
197 "Invalid range: {block_option:?}"
198 )))?,
199 };
200 Ok((from, to))
201 }
202 FilterBlockOption::Range { .. } => Err(RpcError::local_usage_str(&format!(
203 "Invalid range: {block_option:?}"
204 )))?,
205 FilterBlockOption::AtBlockHash(_) => {
206 unimplemented!("Querying a single block by hash is not yet supported")
207 }
208 }
209 }
210}
211
212