1use eth_valkyoth_codec::{DecodeError, DecodeLimits, RlpInteger, RlpItem, RlpList, RlpScalar};
2use eth_valkyoth_primitives::Address;
3
4mod error;
5pub use error::{WithdrawalDecodeError, WithdrawalDecodeErrorCategory, WithdrawalField};
6
7const ADDRESS_BYTES: usize = 20;
8
9pub const WITHDRAWAL_FIELD_COUNT: usize = 4;
11
12#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct WithdrawalIndex(u64);
15
16impl WithdrawalIndex {
17 #[must_use]
19 pub const fn new(value: u64) -> Self {
20 Self(value)
21 }
22
23 #[must_use]
25 pub const fn get(self) -> u64 {
26 self.0
27 }
28}
29
30#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
32pub struct WithdrawalValidatorIndex(u64);
33
34impl WithdrawalValidatorIndex {
35 #[must_use]
37 pub const fn new(value: u64) -> Self {
38 Self(value)
39 }
40
41 #[must_use]
43 pub const fn get(self) -> u64 {
44 self.0
45 }
46}
47
48#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
50pub struct WithdrawalAmountGwei(u64);
51
52impl WithdrawalAmountGwei {
53 pub const fn try_new(value: u64) -> Result<Self, WithdrawalDecodeError> {
55 if value == 0 {
56 return Err(WithdrawalDecodeError::ZeroAmount);
57 }
58 Ok(Self(value))
59 }
60
61 #[must_use]
63 pub const fn get(self) -> u64 {
64 self.0
65 }
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub struct UnvalidatedWithdrawals<'a> {
75 encoded_rlp: &'a [u8],
76 list: RlpList<'a>,
77}
78
79impl<'a> UnvalidatedWithdrawals<'a> {
80 #[must_use]
82 pub const fn encoded_rlp(self) -> &'a [u8] {
83 self.encoded_rlp
84 }
85
86 #[must_use]
88 pub const fn len(self) -> usize {
89 self.list.item_count()
90 }
91
92 #[must_use]
94 pub const fn is_empty(self) -> bool {
95 self.list.is_empty()
96 }
97
98 #[must_use]
100 pub const fn entries(self) -> WithdrawalItems<'a> {
101 WithdrawalItems {
102 items: self.list.items(),
103 }
104 }
105}
106
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub struct UnvalidatedWithdrawal {
110 pub index: WithdrawalIndex,
112 pub validator_index: WithdrawalValidatorIndex,
114 pub address: Address,
116 pub amount: WithdrawalAmountGwei,
118}
119
120#[derive(Clone, Debug)]
122pub struct WithdrawalItems<'a> {
123 items: eth_valkyoth_codec::RlpListItems<'a>,
124}
125
126impl Iterator for WithdrawalItems<'_> {
127 type Item = Result<UnvalidatedWithdrawal, WithdrawalDecodeError>;
128
129 fn next(&mut self) -> Option<Self::Item> {
130 self.items.next().map(decode_withdrawal_item)
131 }
132}
133
134impl core::iter::FusedIterator for WithdrawalItems<'_> {}
135
136pub fn decode_withdrawals<'a>(
143 input: &'a [u8],
144 limits: DecodeLimits,
145) -> Result<UnvalidatedWithdrawals<'a>, WithdrawalDecodeError> {
146 let list = eth_valkyoth_codec::decode_rlp_list(input, limits)
147 .map_err(|source| field_error(WithdrawalField::List, source))?;
148 for item in list.items() {
149 let _ = decode_withdrawal_item(item)?;
150 }
151 Ok(UnvalidatedWithdrawals {
152 encoded_rlp: input,
153 list,
154 })
155}
156
157fn decode_withdrawal_item(
158 item: Result<RlpItem<'_>, DecodeError>,
159) -> Result<UnvalidatedWithdrawal, WithdrawalDecodeError> {
160 let item = item.map_err(|source| field_error(WithdrawalField::Withdrawal, source))?;
161 let RlpItem::List(list) = item else {
162 return Err(field_error(
163 WithdrawalField::Withdrawal,
164 DecodeError::UnexpectedScalar,
165 ));
166 };
167 if list.item_count() != WITHDRAWAL_FIELD_COUNT {
168 return Err(WithdrawalDecodeError::WrongFieldCount {
169 expected: WITHDRAWAL_FIELD_COUNT,
170 found: list.item_count(),
171 });
172 }
173
174 let mut fields = list.items();
175 Ok(UnvalidatedWithdrawal {
176 index: WithdrawalIndex::new(decode_u64(&mut fields, WithdrawalField::Index)?),
177 validator_index: WithdrawalValidatorIndex::new(decode_u64(
178 &mut fields,
179 WithdrawalField::ValidatorIndex,
180 )?),
181 address: Address::from_bytes(decode_address(&mut fields)?),
182 amount: WithdrawalAmountGwei::try_new(decode_u64(&mut fields, WithdrawalField::Amount)?)?,
183 })
184}
185
186fn decode_u64<'a>(
187 fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
188 field: WithdrawalField,
189) -> Result<u64, WithdrawalDecodeError> {
190 RlpInteger::try_from_scalar(next_scalar(fields, field)?)
191 .and_then(RlpInteger::to_u64)
192 .map_err(|source| field_error(field, source))
193}
194
195fn decode_address<'a>(
196 fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
197) -> Result<[u8; ADDRESS_BYTES], WithdrawalDecodeError> {
198 let scalar = next_scalar(fields, WithdrawalField::Address)?;
199 let found = scalar.payload().len();
200 scalar
201 .payload()
202 .try_into()
203 .map_err(|_| WithdrawalDecodeError::InvalidAddressLength { found })
204}
205
206fn next_scalar<'a>(
207 fields: &mut impl Iterator<Item = Result<RlpItem<'a>, DecodeError>>,
208 field: WithdrawalField,
209) -> Result<RlpScalar<'a>, WithdrawalDecodeError> {
210 let item = fields
211 .next()
212 .ok_or(field_error(field, DecodeError::Malformed))?
213 .map_err(|source| field_error(field, source))?;
214 match item {
215 RlpItem::Scalar(scalar) => Ok(scalar),
216 RlpItem::List(_) => Err(field_error(field, DecodeError::UnexpectedList)),
217 }
218}
219
220const fn field_error(field: WithdrawalField, source: DecodeError) -> WithdrawalDecodeError {
221 WithdrawalDecodeError::FieldDecode { field, source }
222}
223
224#[cfg(test)]
225#[path = "withdrawal_tests.rs"]
226mod tests;