Skip to main content

ethrex_rpc/clients/auth/
mod.rs

1use crate::{
2    engine::{
3        ExchangeCapabilitiesRequest,
4        fork_choice::ForkChoiceUpdatedV3,
5        payload::{GetPayloadV5Request, NewPayloadV4Request, NewPayloadV5Request},
6    },
7    types::{
8        fork_choice::{ForkChoiceResponse, ForkChoiceState, PayloadAttributesV3},
9        payload::{ExecutionPayload, ExecutionPayloadResponse, PayloadStatus},
10    },
11    utils::{RpcRequest, RpcResponse},
12};
13use bytes::Bytes;
14use errors::{
15    EngineClientError, ExchangeCapabilitiesError, ForkChoiceUpdatedError, GetPayloadError,
16    NewPayloadError,
17};
18use ethrex_common::H256;
19use reqwest::Client;
20use serde_json::json;
21use std::time::{SystemTime, UNIX_EPOCH};
22
23pub mod errors;
24
25#[derive(Debug, Clone)]
26pub struct EngineClient {
27    client: Client,
28    secret: Bytes,
29    execution_client_url: String,
30}
31
32impl EngineClient {
33    pub fn new(execution_client_url: &str, secret: Bytes) -> Self {
34        Self {
35            client: Client::new(),
36            secret,
37            execution_client_url: execution_client_url.to_string(),
38        }
39    }
40
41    async fn send_request(&self, request: RpcRequest) -> Result<RpcResponse, EngineClientError> {
42        self.client
43            .post(&self.execution_client_url)
44            .bearer_auth(self.auth_token()?)
45            .header("content-type", "application/json")
46            .body(serde_json::ser::to_string(&request).map_err(|error| {
47                EngineClientError::FailedToSerializeRequestBody(format!("{error}: {request:?}"))
48            })?)
49            .send()
50            .await?
51            .json::<RpcResponse>()
52            .await
53            .map_err(EngineClientError::from)
54    }
55
56    pub async fn engine_exchange_capabilities(&self) -> Result<Vec<String>, EngineClientError> {
57        let request = ExchangeCapabilitiesRequest::from(Self::capabilities()).into();
58
59        match self.send_request(request).await? {
60            RpcResponse::Success(result) => serde_json::from_value(result.result)
61                .map_err(ExchangeCapabilitiesError::SerdeJSONError)
62                .map_err(EngineClientError::from),
63            RpcResponse::Error(error_response) => {
64                let error_message = if let Some(data) = error_response.error.data {
65                    format!("{}: {:?}", error_response.error.message, data)
66                } else {
67                    error_response.error.message.to_string()
68                };
69                Err(ExchangeCapabilitiesError::RPCError(error_message).into())
70            }
71        }
72    }
73
74    pub async fn engine_forkchoice_updated_v3(
75        &self,
76        state: ForkChoiceState,
77        payload_attributes: Option<PayloadAttributesV3>,
78    ) -> Result<ForkChoiceResponse, EngineClientError> {
79        let request = RpcRequest::from(ForkChoiceUpdatedV3 {
80            fork_choice_state: state,
81            payload_attributes,
82        });
83
84        match self.send_request(request).await? {
85            RpcResponse::Success(result) => serde_json::from_value(result.result)
86                .map_err(ForkChoiceUpdatedError::SerdeJSONError)
87                .map_err(EngineClientError::from),
88            RpcResponse::Error(error_response) => {
89                let error_message = if let Some(data) = error_response.error.data {
90                    format!("{}: {:?}", error_response.error.message, data)
91                } else {
92                    error_response.error.message.to_string()
93                };
94                Err(ForkChoiceUpdatedError::RPCError(error_message).into())
95            }
96        }
97    }
98
99    pub async fn engine_get_payload_v5(
100        &self,
101        payload_id: u64,
102    ) -> Result<ExecutionPayloadResponse, EngineClientError> {
103        let request = GetPayloadV5Request { payload_id }.into();
104
105        match self.send_request(request).await? {
106            RpcResponse::Success(result) => serde_json::from_value(result.result)
107                .map_err(GetPayloadError::SerdeJSONError)
108                .map_err(EngineClientError::from),
109            RpcResponse::Error(error_response) => {
110                let error_message = if let Some(data) = error_response.error.data {
111                    format!("{}: {:?}", error_response.error.message, data)
112                } else {
113                    error_response.error.message.to_string()
114                };
115                Err(GetPayloadError::RPCError(error_message).into())
116            }
117        }
118    }
119
120    pub async fn engine_new_payload_v4(
121        &self,
122        execution_payload: ExecutionPayload,
123        expected_blob_versioned_hashes: Vec<H256>,
124        parent_beacon_block_root: H256,
125    ) -> Result<PayloadStatus, EngineClientError> {
126        let request = NewPayloadV4Request {
127            payload: execution_payload,
128            expected_blob_versioned_hashes,
129            parent_beacon_block_root,
130            execution_requests: vec![],
131        }
132        .into();
133
134        match self.send_request(request).await? {
135            RpcResponse::Success(result) => serde_json::from_value(result.result)
136                .map_err(NewPayloadError::SerdeJSONError)
137                .map_err(EngineClientError::from),
138            RpcResponse::Error(error_response) => {
139                let error_message = if let Some(data) = error_response.error.data {
140                    format!("{}: {:?}", error_response.error.message, data)
141                } else {
142                    error_response.error.message.to_string()
143                };
144                Err(NewPayloadError::RPCError(error_message).into())
145            }
146        }
147    }
148
149    /// Amsterdam (EIP-7928) variant. The Block Access List travels inside
150    /// `execution_payload.block_access_list`; the server derives its hash from
151    /// the raw payload bytes, so no separate BAL argument is needed here.
152    pub async fn engine_new_payload_v5(
153        &self,
154        execution_payload: ExecutionPayload,
155        expected_blob_versioned_hashes: Vec<H256>,
156        parent_beacon_block_root: H256,
157    ) -> Result<PayloadStatus, EngineClientError> {
158        let request = NewPayloadV5Request {
159            payload: execution_payload,
160            expected_blob_versioned_hashes,
161            parent_beacon_block_root,
162            // The dev producer does not synthesize deposits/withdrawals/consolidations,
163            // so blocks driven through this client carry no execution requests; this
164            // matches the V4 path and is not suitable for deposit-bearing Amsterdam tests.
165            execution_requests: vec![],
166            raw_bal_hash: None,
167        }
168        .into();
169
170        match self.send_request(request).await? {
171            RpcResponse::Success(result) => serde_json::from_value(result.result)
172                .map_err(NewPayloadError::SerdeJSONError)
173                .map_err(EngineClientError::from),
174            RpcResponse::Error(error_response) => {
175                let error_message = if let Some(data) = error_response.error.data {
176                    format!("{}: {:?}", error_response.error.message, data)
177                } else {
178                    error_response.error.message.to_string()
179                };
180                Err(NewPayloadError::RPCError(error_message).into())
181            }
182        }
183    }
184
185    fn auth_token(&self) -> Result<String, EngineClientError> {
186        // Header
187        let header = jsonwebtoken::Header::default();
188        // Claims
189        let valid_iat = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
190        let claims = json!({"iat": valid_iat});
191        let encoding_key = jsonwebtoken::EncodingKey::from_secret(&self.secret);
192        // JWT Token
193        jsonwebtoken::encode(&header, &claims, &encoding_key).map_err(EngineClientError::from)
194    }
195
196    fn capabilities() -> Vec<String> {
197        vec![
198            "engine_exchangeCapabilities".to_owned(),
199            "engine_forkchoiceUpdatedV3".to_owned(),
200            "engine_getPayloadV4".to_owned(),
201            "engine_getPayloadV5".to_owned(),
202            "engine_newPayloadV4".to_owned(),
203        ]
204    }
205}