1use std::collections::HashMap;
2
3use alloy::primitives::U256;
4use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
5use tycho_common::{models::token::Token, Bytes};
6use tycho_ethereum::BytesCodec;
7
8use super::state::RocketpoolState;
9use crate::protocol::{
10 errors::InvalidSnapshotError,
11 models::{DecoderContext, TryFromWithBlock},
12};
13
14impl TryFromWithBlock<ComponentWithState, BlockHeader> for RocketpoolState {
15 type Error = InvalidSnapshotError;
16
17 async fn try_from_with_header(
20 snapshot: ComponentWithState,
21 _block: BlockHeader,
22 _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
23 _all_tokens: &HashMap<Bytes, Token>,
24 _decoder_context: &DecoderContext,
25 ) -> Result<Self, Self::Error> {
26 let total_eth = snapshot
27 .state
28 .attributes
29 .get("total_eth")
30 .map(U256::from_bytes)
31 .ok_or_else(|| InvalidSnapshotError::MissingAttribute("total_eth".to_string()))?;
32 let reth_supply = snapshot
33 .state
34 .attributes
35 .get("reth_supply")
36 .map(U256::from_bytes)
37 .ok_or_else(|| InvalidSnapshotError::MissingAttribute("reth_supply".to_string()))?;
38
39 let deposit_contract_balance = snapshot
40 .state
41 .attributes
42 .get("deposit_contract_balance")
43 .map(U256::from_bytes)
44 .ok_or_else(|| {
45 InvalidSnapshotError::MissingAttribute("deposit_contract_balance".to_string())
46 })?;
47
48 let reth_contract_liquidity = snapshot
49 .state
50 .attributes
51 .get("reth_contract_liquidity")
52 .map(U256::from_bytes)
53 .ok_or_else(|| {
54 InvalidSnapshotError::MissingAttribute("reth_contract_liquidity".to_string())
55 })?;
56
57 let deposits_enabled = snapshot
58 .state
59 .attributes
60 .get("deposits_enabled")
61 .map(|val| !U256::from_bytes(val).is_zero())
62 .ok_or_else(|| {
63 InvalidSnapshotError::MissingAttribute("deposits_enabled".to_string())
64 })?;
65
66 let deposit_assigning_enabled = snapshot
67 .state
68 .attributes
69 .get("deposit_assigning_enabled")
70 .map(|val| !U256::from_bytes(val).is_zero())
71 .ok_or_else(|| {
72 InvalidSnapshotError::MissingAttribute("deposit_assigning_enabled".to_string())
73 })?;
74
75 let deposit_fee = snapshot
76 .state
77 .attributes
78 .get("deposit_fee")
79 .map(U256::from_bytes)
80 .ok_or_else(|| InvalidSnapshotError::MissingAttribute("deposit_fee".to_string()))?;
81
82 let min_deposit_amount = snapshot
83 .state
84 .attributes
85 .get("min_deposit_amount")
86 .map(U256::from_bytes)
87 .ok_or_else(|| {
88 InvalidSnapshotError::MissingAttribute("min_deposit_amount".to_string())
89 })?;
90
91 let max_deposit_pool_size = snapshot
92 .state
93 .attributes
94 .get("max_deposit_pool_size")
95 .map(U256::from_bytes)
96 .ok_or_else(|| {
97 InvalidSnapshotError::MissingAttribute("max_deposit_pool_size".to_string())
98 })?;
99
100 let deposit_assign_maximum = snapshot
101 .state
102 .attributes
103 .get("deposit_assign_maximum")
104 .map(U256::from_bytes)
105 .ok_or_else(|| {
106 InvalidSnapshotError::MissingAttribute("deposit_assign_maximum".to_string())
107 })?;
108
109 let deposit_assign_socialised_maximum = snapshot
110 .state
111 .attributes
112 .get("deposit_assign_socialised_maximum")
113 .map(U256::from_bytes)
114 .ok_or_else(|| {
115 InvalidSnapshotError::MissingAttribute(
116 "deposit_assign_socialised_maximum".to_string(),
117 )
118 })?;
119
120 let queue_variable_start = snapshot
121 .state
122 .attributes
123 .get("queue_variable_start")
124 .map(U256::from_bytes)
125 .ok_or_else(|| {
126 InvalidSnapshotError::MissingAttribute("queue_variable_start".to_string())
127 })?;
128
129 let queue_variable_end = snapshot
130 .state
131 .attributes
132 .get("queue_variable_end")
133 .map(U256::from_bytes)
134 .ok_or_else(|| {
135 InvalidSnapshotError::MissingAttribute("queue_variable_end".to_string())
136 })?;
137
138 Ok(RocketpoolState::new(
139 reth_supply,
140 total_eth,
141 deposit_contract_balance,
142 reth_contract_liquidity,
143 deposit_fee,
144 deposits_enabled,
145 min_deposit_amount,
146 max_deposit_pool_size,
147 deposit_assigning_enabled,
148 deposit_assign_maximum,
149 deposit_assign_socialised_maximum,
150 queue_variable_start,
151 queue_variable_end,
152 ))
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use std::collections::HashMap;
159
160 use alloy::primitives::U256;
161 use rstest::rstest;
162 use tycho_client::feed::synchronizer::ComponentWithState;
163 use tycho_common::{dto::ResponseProtocolState, Bytes};
164
165 use super::super::state::RocketpoolState;
166 use crate::{
167 evm::protocol::{test_utils, test_utils::try_decode_snapshot_with_defaults},
168 protocol::errors::InvalidSnapshotError,
169 };
170
171 fn create_test_snapshot() -> ComponentWithState {
172 ComponentWithState {
173 state: ResponseProtocolState {
174 component_id: "Rocketpool".to_owned(),
175 attributes: HashMap::from([
176 (
177 "total_eth".to_string(),
178 Bytes::from(U256::from(100_000_000_000_000_000_000u128).to_be_bytes_vec()),
179 ),
180 (
181 "reth_supply".to_string(),
182 Bytes::from(U256::from(95_000_000_000_000_000_000u128).to_be_bytes_vec()),
183 ),
184 (
185 "deposit_contract_balance".to_string(),
186 Bytes::from(U256::from(50_000_000_000_000_000_000u128).to_be_bytes_vec()),
187 ), (
189 "reth_contract_liquidity".to_string(),
190 Bytes::from(U256::from(10_000_000_000_000_000_000u128).to_be_bytes_vec()),
191 ), ("deposits_enabled".to_string(), Bytes::from(vec![0x01])),
193 ("deposit_assigning_enabled".to_string(), Bytes::from(vec![0x01])),
194 (
195 "deposit_fee".to_string(),
196 Bytes::from(U256::from(5_000_000_000_000_000u128).to_be_bytes_vec()),
197 ), (
199 "min_deposit_amount".to_string(),
200 Bytes::from(U256::from(10_000_000_000_000_000u128).to_be_bytes_vec()),
201 ), (
203 "max_deposit_pool_size".to_string(),
204 Bytes::from(
205 U256::from(5_000_000_000_000_000_000_000u128).to_be_bytes_vec(),
206 ),
207 ), (
209 "deposit_assign_maximum".to_string(),
210 Bytes::from(U256::from(10u64).to_be_bytes_vec()),
211 ),
212 (
213 "deposit_assign_socialised_maximum".to_string(),
214 Bytes::from(U256::from(2u64).to_be_bytes_vec()),
215 ),
216 (
217 "queue_variable_start".to_string(),
218 Bytes::from(U256::from(100u64).to_be_bytes_vec()),
219 ),
220 (
221 "queue_variable_end".to_string(),
222 Bytes::from(U256::from(105u64).to_be_bytes_vec()),
223 ),
224 ]),
225 balances: HashMap::new(),
226 },
227 component: Default::default(),
228 component_tvl: None,
229 entrypoints: Vec::new(),
230 }
231 }
232
233 #[tokio::test]
234 async fn test_rocketpool_try_from() {
235 let snapshot = create_test_snapshot();
236
237 let result =
238 test_utils::try_decode_snapshot_with_defaults::<RocketpoolState>(snapshot).await;
239
240 assert!(result.is_ok());
241 let state = result.unwrap();
242 assert_eq!(state.total_eth, U256::from(100_000_000_000_000_000_000u128));
243 assert_eq!(state.reth_supply, U256::from(95_000_000_000_000_000_000u128));
244 assert_eq!(state.deposit_contract_balance, U256::from(50_000_000_000_000_000_000u128));
245 assert_eq!(state.reth_contract_liquidity, U256::from(10_000_000_000_000_000_000u128));
246 assert!(state.deposits_enabled);
247 assert!(state.deposit_assigning_enabled);
248 assert_eq!(state.min_deposit_amount, U256::from(10_000_000_000_000_000u128));
249 assert_eq!(state.max_deposit_pool_size, U256::from(5_000_000_000_000_000_000_000u128));
250 assert_eq!(state.queue_variable_start, U256::from(100u64));
251 assert_eq!(state.queue_variable_end, U256::from(105u64));
252 }
253
254 #[tokio::test]
255 async fn test_rocketpool_try_from_deposits_disabled() {
256 let eth_address = Bytes::from(vec![0u8; 20]);
257
258 let snapshot = ComponentWithState {
259 state: ResponseProtocolState {
260 component_id: "Rocketpool".to_owned(),
261 attributes: HashMap::from([
262 ("total_eth".to_string(), Bytes::from(U256::from(100u64).to_be_bytes_vec())),
263 ("reth_supply".to_string(), Bytes::from(U256::from(100u64).to_be_bytes_vec())),
264 (
265 "deposit_contract_balance".to_string(),
266 Bytes::from(U256::from(50u64).to_be_bytes_vec()),
267 ),
268 (
269 "reth_contract_liquidity".to_string(),
270 Bytes::from(U256::from(10u64).to_be_bytes_vec()),
271 ),
272 ("deposits_enabled".to_string(), Bytes::from(vec![0x00])), ("deposit_assigning_enabled".to_string(), Bytes::from(vec![0x00])), ("deposit_fee".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
275 (
276 "min_deposit_amount".to_string(),
277 Bytes::from(U256::from(0u64).to_be_bytes_vec()),
278 ),
279 (
280 "max_deposit_pool_size".to_string(),
281 Bytes::from(U256::from(1000u64).to_be_bytes_vec()),
282 ),
283 (
284 "deposit_assign_maximum".to_string(),
285 Bytes::from(U256::from(0u64).to_be_bytes_vec()),
286 ),
287 (
288 "deposit_assign_socialised_maximum".to_string(),
289 Bytes::from(U256::from(0u64).to_be_bytes_vec()),
290 ),
291 (
292 "queue_full_start".to_string(),
293 Bytes::from(U256::from(0u64).to_be_bytes_vec()),
294 ),
295 ("queue_full_end".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
296 (
297 "queue_half_start".to_string(),
298 Bytes::from(U256::from(0u64).to_be_bytes_vec()),
299 ),
300 ("queue_half_end".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
301 (
302 "queue_variable_start".to_string(),
303 Bytes::from(U256::from(0u64).to_be_bytes_vec()),
304 ),
305 (
306 "queue_variable_end".to_string(),
307 Bytes::from(U256::from(0u64).to_be_bytes_vec()),
308 ),
309 ]),
310 balances: HashMap::from([(
311 eth_address,
312 Bytes::from(U256::from(50u64).to_be_bytes_vec()),
313 )]),
314 },
315 component: Default::default(),
316 component_tvl: None,
317 entrypoints: Vec::new(),
318 };
319
320 let result = try_decode_snapshot_with_defaults::<RocketpoolState>(snapshot).await;
321
322 assert!(result.is_ok());
323 let state = result.unwrap();
324 assert!(!state.deposits_enabled);
325 assert!(!state.deposit_assigning_enabled);
326 }
327
328 #[tokio::test]
329 #[rstest]
330 #[case::missing_total_eth("total_eth")]
331 #[case::missing_reth_supply("reth_supply")]
332 #[case::missing_deposit_contract_balance("deposit_contract_balance")]
333 #[case::missing_reth_contract_liquidity("reth_contract_liquidity")]
334 #[case::missing_deposits_enabled("deposits_enabled")]
335 #[case::missing_deposit_assigning_enabled("deposit_assigning_enabled")]
336 #[case::missing_deposit_fee("deposit_fee")]
337 #[case::missing_min_deposit_amount("min_deposit_amount")]
338 #[case::missing_max_deposit_pool_size("max_deposit_pool_size")]
339 #[case::missing_deposit_assign_maximum("deposit_assign_maximum")]
340 #[case::missing_deposit_assign_socialised_maximum("deposit_assign_socialised_maximum")]
341 #[case::missing_queue_variable_start("queue_variable_start")]
342 #[case::missing_queue_variable_end("queue_variable_end")]
343 async fn test_rocketpool_try_from_missing_attribute(#[case] missing_attribute: &str) {
344 let eth_address = Bytes::from(vec![0u8; 20]);
345
346 let mut attributes = HashMap::from([
347 ("total_eth".to_string(), Bytes::from(U256::from(100u64).to_be_bytes_vec())),
348 ("reth_supply".to_string(), Bytes::from(U256::from(100u64).to_be_bytes_vec())),
349 (
350 "deposit_contract_balance".to_string(),
351 Bytes::from(U256::from(50u64).to_be_bytes_vec()),
352 ),
353 (
354 "reth_contract_liquidity".to_string(),
355 Bytes::from(U256::from(10u64).to_be_bytes_vec()),
356 ),
357 ("deposits_enabled".to_string(), Bytes::from(vec![0x01])),
358 ("deposit_assigning_enabled".to_string(), Bytes::from(vec![0x01])),
359 ("deposit_fee".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
360 ("min_deposit_amount".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
361 (
362 "max_deposit_pool_size".to_string(),
363 Bytes::from(U256::from(1000u64).to_be_bytes_vec()),
364 ),
365 ("deposit_assign_maximum".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
366 (
367 "deposit_assign_socialised_maximum".to_string(),
368 Bytes::from(U256::from(0u64).to_be_bytes_vec()),
369 ),
370 ("queue_full_start".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
371 ("queue_full_end".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
372 ("queue_half_start".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
373 ("queue_half_end".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
374 ("queue_variable_start".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
375 ("queue_variable_end".to_string(), Bytes::from(U256::from(0u64).to_be_bytes_vec())),
376 ]);
377 attributes.remove(missing_attribute);
378
379 let snapshot = ComponentWithState {
380 state: ResponseProtocolState {
381 component_id: "Rocketpool".to_owned(),
382 attributes,
383 balances: HashMap::from([(
384 eth_address,
385 Bytes::from(U256::from(50u64).to_be_bytes_vec()),
386 )]),
387 },
388 component: Default::default(),
389 component_tvl: None,
390 entrypoints: Vec::new(),
391 };
392
393 let result = try_decode_snapshot_with_defaults::<RocketpoolState>(snapshot).await;
394
395 assert!(result.is_err());
396 assert!(matches!(
397 result.unwrap_err(),
398 InvalidSnapshotError::MissingAttribute(ref x) if x == missing_attribute
399 ));
400 }
401}