1use crate::types::{
4 BalanceChangesInBlock, GetBlockExecutionOutcomeParams, RethNewPayloadParams, RethPayloadStatus,
5};
6use alloy_network::Network;
7use alloy_provider::Provider;
8use alloy_rpc_types_engine::{ForkchoiceState, ForkchoiceUpdated};
9use alloy_transport::TransportResult;
10
11mod sealed {
12 pub trait Sealed {}
13 impl<T> Sealed for T {}
14}
15
16#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
21#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
22pub trait RethApi<N: Network>: sealed::Sealed + Send + Sync {
23 async fn reth_get_balance_changes_in_block(
29 &self,
30 params: impl Into<GetBlockExecutionOutcomeParams> + Send,
31 ) -> TransportResult<BalanceChangesInBlock>;
32
33 async fn reth_get_block_execution_outcome(
39 &self,
40 params: impl Into<GetBlockExecutionOutcomeParams> + Send,
41 ) -> TransportResult<Option<serde_json::Value>>;
42
43 async fn reth_new_payload<E>(
70 &self,
71 params: impl Into<RethNewPayloadParams<E>> + Send,
72 ) -> TransportResult<RethPayloadStatus>
73 where
74 E: serde::Serialize + Clone + core::fmt::Debug + Send + Sync + Unpin + 'static;
75
76 async fn reth_forkchoice_updated(
102 &self,
103 forkchoice_state: ForkchoiceState,
104 ) -> TransportResult<ForkchoiceUpdated>;
105}
106
107#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
108#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
109impl<N, P> RethApi<N> for P
110where
111 N: Network,
112 P: Provider<N>,
113{
114 async fn reth_get_balance_changes_in_block(
115 &self,
116 params: impl Into<GetBlockExecutionOutcomeParams> + Send,
117 ) -> TransportResult<BalanceChangesInBlock> {
118 let params = params.into();
119 self.client()
120 .request("reth_getBalanceChangesInBlock", (params.block_id,))
121 .await
122 }
123
124 async fn reth_get_block_execution_outcome(
125 &self,
126 params: impl Into<GetBlockExecutionOutcomeParams> + Send,
127 ) -> TransportResult<Option<serde_json::Value>> {
128 let params = params.into();
129 self.client()
130 .request(
131 "reth_getBlockExecutionOutcome",
132 (params.block_id, params.count),
133 )
134 .await
135 }
136
137 async fn reth_new_payload<E>(
138 &self,
139 params: impl Into<RethNewPayloadParams<E>> + Send,
140 ) -> TransportResult<RethPayloadStatus>
141 where
142 E: serde::Serialize + Clone + core::fmt::Debug + Send + Sync + Unpin + 'static,
143 {
144 let params = params.into();
145 self.client()
146 .request(
147 "reth_newPayload",
148 (
149 params.payload,
150 params.wait_for_persistence,
151 params.wait_for_caches,
152 ),
153 )
154 .await
155 }
156
157 async fn reth_forkchoice_updated(
158 &self,
159 forkchoice_state: ForkchoiceState,
160 ) -> TransportResult<ForkchoiceUpdated> {
161 self.client()
162 .request("reth_forkchoiceUpdated", (forkchoice_state,))
163 .await
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::{BigBlockData, RethNewPayloadInput};
171 use alloy_json_rpc::RequestPacket;
172 use alloy_network::Ethereum;
173 use alloy_primitives::{Address, B256, Bytes, U64, U256};
174 use alloy_provider::{Provider, ProviderBuilder};
175 use alloy_rpc_client::RpcClient;
176 use alloy_rpc_types_engine::{ForkchoiceState, PayloadStatusEnum};
177 use alloy_transport::{TransportError, TransportFut, mock::Asserter};
178 use serde::{Deserialize, Serialize};
179 use serde_json::{Value, json};
180 use std::sync::{Arc, Mutex};
181 use tower::Service;
182
183 #[derive(Debug, Clone, Serialize, Deserialize)]
184 struct TestExecutionData {
185 payload: Bytes,
186 sidecar: Bytes,
187 }
188
189 #[derive(Clone, Debug)]
190 struct RecordingTransport {
191 inner: alloy_transport::mock::MockTransport,
192 requests: Arc<Mutex<Vec<RequestPacket>>>,
193 }
194
195 impl Service<RequestPacket> for RecordingTransport {
196 type Response = alloy_json_rpc::ResponsePacket;
197 type Error = TransportError;
198 type Future = TransportFut<'static>;
199
200 fn poll_ready(
201 &mut self,
202 cx: &mut std::task::Context<'_>,
203 ) -> std::task::Poll<Result<(), Self::Error>> {
204 self.inner.poll_ready(cx)
205 }
206
207 fn call(&mut self, request: RequestPacket) -> Self::Future {
208 self.requests.lock().unwrap().push(request.clone());
209 self.inner.call(request)
210 }
211 }
212
213 fn provider_with_asserter(
214 asserter: Asserter,
215 ) -> (impl Provider<Ethereum>, Arc<Mutex<Vec<RequestPacket>>>) {
216 let requests = Arc::new(Mutex::new(Vec::new()));
217 let transport = RecordingTransport {
218 inner: alloy_transport::mock::MockTransport::new(asserter),
219 requests: Arc::clone(&requests),
220 };
221 let provider = ProviderBuilder::new().connect_client(RpcClient::new(transport, true));
222 (provider, requests)
223 }
224
225 fn take_request(requests: &Arc<Mutex<Vec<RequestPacket>>>) -> Value {
226 let packet = requests.lock().unwrap().remove(0);
227 let request = packet
228 .as_single()
229 .expect("expected a single JSON-RPC request");
230 serde_json::from_str(request.serialized().get()).unwrap()
231 }
232
233 fn assert_request(requests: &Arc<Mutex<Vec<RequestPacket>>>, method: &str, params: Value) {
234 let request = take_request(requests);
235 assert_eq!(request["method"], method);
236 assert_eq!(request["params"], params);
237 }
238
239 fn push_valid_status(asserter: &Asserter, with_timings: bool) {
240 let response = if with_timings {
241 json!({
242 "status": "VALID",
243 "latestValidHash": null,
244 "latency_us": 12,
245 "persistence_wait_us": 3,
246 "execution_cache_wait_us": 4,
247 "sparse_trie_wait_us": 5,
248 })
249 } else {
250 json!({"status": "VALID", "latestValidHash": null})
251 };
252 asserter.push_success(&response);
253 }
254
255 fn execution_data() -> TestExecutionData {
256 TestExecutionData {
257 payload: Bytes::from_static(&[0x01]),
258 sidecar: Bytes::from_static(&[0x02]),
259 }
260 }
261
262 #[tokio::test]
263 async fn balance_changes_uses_exact_reth_method_and_params() {
264 let asserter = Asserter::new();
265 asserter.push_success(&json!({
266 "0x1111111111111111111111111111111111111111": "0x02"
267 }));
268 let (provider, requests) = provider_with_asserter(asserter);
269
270 let changes = provider
271 .reth_get_balance_changes_in_block(alloy_eips::BlockId::latest())
272 .await
273 .unwrap();
274
275 assert_eq!(
276 changes.get(&Address::from([0x11; 20])),
277 Some(&U256::from(2))
278 );
279 assert_request(
280 &requests,
281 "reth_getBalanceChangesInBlock",
282 json!(["latest"]),
283 );
284 }
285
286 #[tokio::test]
287 async fn execution_outcome_uses_exact_reth_method_and_params() {
288 let asserter = Asserter::new();
289 asserter.push_success(&json!({"state": "0x01"}));
290 let (provider, requests) = provider_with_asserter(asserter);
291
292 let outcome = provider
293 .reth_get_block_execution_outcome(
294 GetBlockExecutionOutcomeParams::new(alloy_eips::BlockId::latest())
295 .with_count(U64::from_limbs([2])),
296 )
297 .await
298 .unwrap();
299
300 assert_eq!(outcome, Some(json!({"state": "0x01"})));
301 assert_request(
302 &requests,
303 "reth_getBlockExecutionOutcome",
304 json!(["latest", "0x2"]),
305 );
306 }
307
308 #[tokio::test]
309 async fn new_payload_sends_all_variants_and_both_wait_flags() {
310 let asserter = Asserter::new();
311 push_valid_status(&asserter, true);
312 push_valid_status(&asserter, false);
313 push_valid_status(&asserter, false);
314 let (provider, requests) = provider_with_asserter(asserter);
315
316 let standard = RethNewPayloadInput::execution_data(execution_data());
317 let status = provider
318 .reth_new_payload(
319 RethNewPayloadParams::new(standard)
320 .with_wait_for_persistence(true)
321 .with_wait_for_caches(false),
322 )
323 .await
324 .unwrap();
325 assert_eq!(status.status.status, PayloadStatusEnum::Valid);
326 assert_eq!(status.latency_us, 12);
327 assert_eq!(status.persistence_wait_us, Some(3));
328 assert_eq!(status.execution_cache_wait_us, Some(4));
329 assert_eq!(status.sparse_trie_wait_us, Some(5));
330 assert_request(
331 &requests,
332 "reth_newPayload",
333 json!([{"payload": "0x01", "sidecar": "0x02"}, true, false]),
334 );
335
336 let hash = B256::from([0x11; 32]);
337 let big_block = RethNewPayloadInput::big_block_data(BigBlockData {
338 env_switches: vec![execution_data()],
339 prior_block_hashes: vec![(7, hash)],
340 block_number: 8,
341 merged_block_access_list: None,
342 });
343 let status = provider.reth_new_payload(big_block).await.unwrap();
344 assert_eq!(status.persistence_wait_us, None);
345 assert_eq!(status.execution_cache_wait_us, None);
346 assert_eq!(status.sparse_trie_wait_us, None);
347 let hash = serde_json::to_value(hash).unwrap();
348 assert_request(
349 &requests,
350 "reth_newPayload",
351 json!([{
352 "env_switches": [{"payload": "0x01", "sidecar": "0x02"}],
353 "prior_block_hashes": [[7, hash]],
354 "block_number": 8,
355 }, null, null]),
356 );
357
358 let raw = RethNewPayloadInput::<serde_json::Value>::block_rlp_with_bal(
359 Bytes::from_static(&[0x04]),
360 Bytes::from_static(&[0x05]),
361 );
362 provider
363 .reth_new_payload(RethNewPayloadParams::new(raw).with_wait_for_caches(true))
364 .await
365 .unwrap();
366 assert_request(
367 &requests,
368 "reth_newPayload",
369 json!([{"block": "0x04", "bal": "0x05"}, null, true]),
370 );
371 }
372
373 #[tokio::test]
374 async fn forkchoice_updated_sends_only_state_without_payload_attributes() {
375 let asserter = Asserter::new();
376 asserter.push_success(&json!({
377 "payloadStatus": {"status": "VALID", "latestValidHash": null},
378 "payloadId": null,
379 }));
380 let (provider, requests) = provider_with_asserter(asserter);
381 let state = ForkchoiceState::same_hash(B256::from([0x11; 32]));
382
383 let response = provider.reth_forkchoice_updated(state).await.unwrap();
384 assert!(response.payload_status.is_valid());
385
386 assert_request(
387 &requests,
388 "reth_forkchoiceUpdated",
389 json!([serde_json::to_value(state).unwrap()]),
390 );
391 }
392}