1#[cfg(feature = "ssz")]
2use alloy_eips::eip7685::Requests;
3use alloy_eips::{
4 eip6110::DepositRequest, eip7002::WithdrawalRequest, eip7251::ConsolidationRequest,
5};
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
10#[serde(deny_unknown_fields)]
11#[serde(rename_all = "snake_case")]
12#[cfg_attr(feature = "ssz", derive(ssz_derive::Decode, ssz_derive::Encode))]
13pub struct ExecutionRequestsV4 {
14 pub deposits: Vec<DepositRequest>,
16 pub withdrawals: Vec<WithdrawalRequest>,
18 pub consolidations: Vec<ConsolidationRequest>,
20}
21
22impl<'de> Deserialize<'de> for ExecutionRequestsV4 {
23 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
24 where
25 D: serde::Deserializer<'de>,
26 {
27 #[derive(Deserialize)]
28 struct Helper {
29 #[serde(default)]
30 deposits: Option<Vec<DepositRequest>>,
31 #[serde(default)]
32 withdrawals: Option<Vec<WithdrawalRequest>>,
33 #[serde(default)]
34 consolidations: Option<Vec<ConsolidationRequest>>,
35 }
36
37 let helper = Helper::deserialize(deserializer)?;
38
39 Ok(Self {
40 deposits: helper.deposits.unwrap_or_default(),
41 withdrawals: helper.withdrawals.unwrap_or_default(),
42 consolidations: helper.consolidations.unwrap_or_default(),
43 })
44 }
45}
46
47impl ExecutionRequestsV4 {
48 #[cfg(feature = "ssz")]
50 pub fn to_requests(&self) -> Requests {
51 self.into()
52 }
53}
54
55#[cfg(feature = "ssz")]
56pub use ssz_requests_conversions::TryFromRequestsError;
57
58#[cfg(feature = "ssz")]
59mod ssz_requests_conversions {
60 use super::*;
61 use crate::requests::TryFromRequestsError::SszDecodeError;
62 use alloy_eips::{
63 eip6110::{DepositRequest, DEPOSIT_REQUEST_TYPE, MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD},
64 eip7002::{WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_BLOCK, WITHDRAWAL_REQUEST_TYPE},
65 eip7251::{
66 ConsolidationRequest, CONSOLIDATION_REQUEST_TYPE, MAX_CONSOLIDATION_REQUESTS_PER_BLOCK,
67 },
68 eip7685::Requests,
69 };
70 use ssz::{Decode, DecodeError, Encode};
71
72 impl TryFrom<Requests> for ExecutionRequestsV4 {
73 type Error = TryFromRequestsError;
74
75 fn try_from(value: Requests) -> Result<Self, Self::Error> {
76 Self::try_from(&value)
77 }
78 }
79
80 impl TryFrom<&Requests> for ExecutionRequestsV4 {
81 type Error = TryFromRequestsError;
82
83 fn try_from(value: &Requests) -> Result<Self, Self::Error> {
84 #[derive(Default)]
85 struct RequestAccumulator {
86 deposits: Vec<DepositRequest>,
87 withdrawals: Vec<WithdrawalRequest>,
88 consolidations: Vec<ConsolidationRequest>,
89 }
90
91 impl RequestAccumulator {
92 fn parse_request_payload<T>(
93 payload: &[u8],
94 max_size: usize,
95 request_type: u8,
96 ) -> Result<Vec<T>, TryFromRequestsError>
97 where
98 T: Decode,
99 {
100 let list: Vec<T> = Vec::from_ssz_bytes(payload)
101 .map_err(|e| SszDecodeError(request_type, e))?;
102
103 if list.len() > max_size {
104 return Err(TryFromRequestsError::RequestPayloadSizeExceeded(
105 request_type,
106 list.len(),
107 ));
108 }
109
110 Ok(list)
111 }
112
113 fn accumulate(mut self, request: &[u8]) -> Result<Self, TryFromRequestsError> {
114 if request.is_empty() {
115 return Err(TryFromRequestsError::EmptyRequest);
116 }
117
118 let (request_type, payload) =
119 request.split_first().expect("already checked for empty");
120
121 match *request_type {
122 DEPOSIT_REQUEST_TYPE => {
123 self.deposits = Self::parse_request_payload(
124 payload,
125 MAX_DEPOSIT_RECEIPTS_PER_PAYLOAD,
126 DEPOSIT_REQUEST_TYPE,
127 )?;
128 }
129 WITHDRAWAL_REQUEST_TYPE => {
130 self.withdrawals = Self::parse_request_payload(
131 payload,
132 MAX_WITHDRAWAL_REQUESTS_PER_BLOCK,
133 WITHDRAWAL_REQUEST_TYPE,
134 )?;
135 }
136 CONSOLIDATION_REQUEST_TYPE => {
137 self.consolidations = Self::parse_request_payload(
138 payload,
139 MAX_CONSOLIDATION_REQUESTS_PER_BLOCK,
140 CONSOLIDATION_REQUEST_TYPE,
141 )?;
142 }
143 unknown => return Err(TryFromRequestsError::UnknownRequestType(unknown)),
144 }
145
146 Ok(self)
147 }
148 }
149
150 let accumulator = value
151 .iter()
152 .try_fold(RequestAccumulator::default(), |acc, request| acc.accumulate(request))?;
153
154 Ok(Self {
155 deposits: accumulator.deposits,
156 withdrawals: accumulator.withdrawals,
157 consolidations: accumulator.consolidations,
158 })
159 }
160 }
161
162 #[derive(Debug, thiserror::Error)]
164 pub enum TryFromRequestsError {
165 #[error("empty bytes in requests body")]
167 EmptyRequest,
168 #[error("unknown request_type prefix: {0}")]
170 UnknownRequestType(u8),
171 #[error("ssz decode error for request_type {0}: {1:?}")]
173 SszDecodeError(u8, DecodeError),
174 #[error("requests_data payload for request_type {0} exceeds Electra size limit {1}")]
176 RequestPayloadSizeExceeded(u8, usize),
177 }
178
179 impl From<&ExecutionRequestsV4> for Requests {
180 fn from(val: &ExecutionRequestsV4) -> Self {
181 let deposit_bytes = val.deposits.as_ssz_bytes();
182 let withdrawals_bytes = val.withdrawals.as_ssz_bytes();
183 let consolidations_bytes = val.consolidations.as_ssz_bytes();
184
185 let mut requests = Self::with_capacity(3);
186 requests.push_request_with_type(DEPOSIT_REQUEST_TYPE, deposit_bytes);
187 requests.push_request_with_type(WITHDRAWAL_REQUEST_TYPE, withdrawals_bytes);
188 requests.push_request_with_type(CONSOLIDATION_REQUEST_TYPE, consolidations_bytes);
189 requests
190 }
191 }
192
193 #[cfg(test)]
194 mod tests {
195 use super::*;
196 use alloy_primitives::Bytes;
197 use std::str::FromStr;
198 #[test]
199 fn test_from_requests() -> Result<(), TryFromRequestsError> {
200 let original = Requests::new(vec![
201 Bytes::from_str("0x0096a96086cff07df17668f35f7418ef8798079167e3f4f9b72ecde17b28226137cf454ab1dd20ef5d924786ab3483c2f9003f5102dabe0a27b1746098d1dc17a5d3fbd478759fea9287e4e419b3c3cef20100000000000000b1acdb2c4d3df3f1b8d3bfd33421660df358d84d78d16c4603551935f4b67643373e7eb63dcb16ec359be0ec41fee33b03a16e80745f2374ff1d3c352508ac5d857c6476d3c3bcf7e6ca37427c9209f17be3af5264c0e2132b3dd1156c28b4e9f000000000000000a5c85a60ba2905c215f6a12872e62b1ee037051364244043a5f639aa81b04a204c55e7cc851f29c7c183be253ea1510b001db70c485b6264692f26b8aeaab5b0c384180df8e2184a21a808a3ec8e86ca01000000000000009561731785b48cf1886412234531e4940064584463e96ac63a1a154320227e333fb51addc4a89b7e0d3f862d7c1fd4ea03bd8eb3d8806f1e7daf591cbbbb92b0beb74d13c01617f22c5026b4f9f9f294a8a7c32db895de3b01bee0132c9209e1f100000000000000").unwrap(),
203 Bytes::from_str("0x01a94f5374fce5edbc8e2a8697c15331677e6ebf0b85103a5617937691dfeeb89b86a80d5dc9e3c9d3a1a0e7ce311e26e0bb732eabaa47ffa288f0d54de28209a62a7d29d0000000000000000000000000000000000000000000000000000010f698daeed734da114470da559bd4b4c7259e1f7952555241dcbc90cf194a2ef676fc6005f3672fada2a3645edb297a75530100000000000000").unwrap(),
204 Bytes::from_str("0x02a94f5374fce5edbc8e2a8697c15331677e6ebf0b85103a5617937691dfeeb89b86a80d5dc9e3c9d3a1a0e7ce311e26e0bb732eabaa47ffa288f0d54de28209a62a7d29d098daeed734da114470da559bd4b4c7259e1f7952555241dcbc90cf194a2ef676fc6005f3672fada2a3645edb297a7553").unwrap(),
205 ]);
206
207 let requests = ExecutionRequestsV4::try_from(&original)?;
208 assert_eq!(requests.deposits.len(), 2);
209 assert_eq!(requests.withdrawals.len(), 2);
210 assert_eq!(requests.consolidations.len(), 1);
211
212 let round_trip: Requests = (&requests).into();
213 assert_eq!(original, round_trip);
214 Ok(())
215 }
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn deserde_requests_v4() {
225 let s = r#"{"deposits":null,"withdrawals":null,"consolidations":null}"#;
226 let requests: ExecutionRequestsV4 = serde_json::from_str(s).unwrap();
227 assert_eq!(requests, ExecutionRequestsV4::default());
228
229 let s = r#"{"deposits":null,"withdrawals":null}"#;
230 let requests: ExecutionRequestsV4 = serde_json::from_str(s).unwrap();
231 assert_eq!(requests, ExecutionRequestsV4::default());
232
233 let s = r#"{"deposits":[],"withdrawals":[],"consolidations":[]}"#;
234 let requests: ExecutionRequestsV4 = serde_json::from_str(s).unwrap();
235 assert_eq!(requests, ExecutionRequestsV4::default());
236 }
237}