tycho_simulation/evm/protocol/ramses_v3/
decoder.rs1use std::collections::HashMap;
2
3use alloy::primitives::U256;
4use itertools::Itertools;
5use num_traits::Zero;
6use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
7use tycho_common::{models::token::Token, Bytes};
8
9use super::state::RamsesV3State;
10use crate::{
11 evm::protocol::utils::uniswap::tick_list::TickInfo,
12 protocol::{
13 errors::InvalidSnapshotError,
14 models::{DecoderContext, TryFromWithBlock},
15 },
16};
17
18impl TryFromWithBlock<ComponentWithState, BlockHeader> for RamsesV3State {
19 type Error = InvalidSnapshotError;
20
21 async fn try_from_with_header(
28 snapshot: ComponentWithState,
29 _block: BlockHeader,
30 _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
31 _all_tokens: &HashMap<Bytes, Token>,
32 _decoder_context: &DecoderContext,
33 ) -> Result<Self, Self::Error> {
34 let static_attrs = snapshot.component.static_attributes;
35 let state_attrs = snapshot.state.attributes;
36
37 let fee = u32::from(attribute(&state_attrs, "fee")?.clone());
39 let liquidity = u128::from(attribute(&state_attrs, "liquidity")?.clone());
40 let sqrt_price = U256::from_be_slice(attribute(&state_attrs, "sqrt_price_x96")?);
41 let tick_spacing = u16::from(attribute(&static_attrs, "tick_spacing")?.clone());
42 let tick = i32::from(attribute(&state_attrs, "tick")?.clone());
43
44 let ticks = state_attrs
47 .iter()
48 .filter_map(|(key, value)| {
49 let tick_index = match key
50 .strip_prefix("ticks/")?
51 .parse::<i32>()
52 {
53 Ok(tick_index) => tick_index,
54 Err(err) => return Some(Err(InvalidSnapshotError::ValueError(err.to_string()))),
55 };
56
57 let net_liquidity = i128::from(value.clone());
58 if net_liquidity.is_zero() {
59 return None;
60 }
61
62 Some(
63 TickInfo::new(tick_index, net_liquidity)
64 .map_err(|err| InvalidSnapshotError::ValueError(err.to_string())),
65 )
66 })
67 .collect::<Result<Vec<_>, _>>()?
68 .into_iter()
69 .sorted_unstable_by_key(|tick| tick.index)
70 .collect();
71
72 RamsesV3State::new(liquidity, sqrt_price, fee, tick_spacing, tick, ticks)
73 .map_err(|err| InvalidSnapshotError::ValueError(err.to_string()))
74 }
75}
76
77fn attribute<'a>(
78 map: &'a HashMap<String, Bytes>,
79 key: &str,
80) -> Result<&'a Bytes, InvalidSnapshotError> {
81 map.get(key)
82 .ok_or_else(|| InvalidSnapshotError::MissingAttribute(key.to_string()))
83}
84
85#[cfg(test)]
86mod tests {
87 use std::str::FromStr;
88
89 use chrono::DateTime;
90 use rstest::rstest;
91 use tycho_common::models::{
92 protocol::{ProtocolComponent, ProtocolComponentState},
93 Chain, ChangeType,
94 };
95
96 use super::*;
97 use crate::evm::protocol::test_utils::try_decode_snapshot_with_defaults;
98
99 fn ramses_component() -> ProtocolComponent {
100 let creation_time = DateTime::from_timestamp(1622526000, 0)
101 .unwrap()
102 .naive_utc();
103
104 let mut static_attributes: HashMap<String, Bytes> = HashMap::new();
106 static_attributes
107 .insert("tick_spacing".to_string(), Bytes::from(60_u16.to_be_bytes().to_vec()));
108
109 ProtocolComponent {
110 id: "State1".to_string(),
111 protocol_system: "ramses_v3".to_string(),
112 protocol_type_name: "ramses_v3_pool".to_string(),
113 chain: Chain::Polygon,
114 tokens: Vec::new(),
115 contract_addresses: Vec::new(),
116 static_attributes,
117 change: ChangeType::Creation,
118 creation_tx: Bytes::from_str("0x0000").unwrap(),
119 created_at: creation_time,
120 }
121 }
122
123 fn ramses_attributes() -> HashMap<String, Bytes> {
124 vec![
125 ("liquidity".to_string(), Bytes::from(100_u64.to_be_bytes().to_vec())),
126 ("sqrt_price_x96".to_string(), Bytes::from(200_u64.to_be_bytes().to_vec())),
127 ("tick".to_string(), Bytes::from(300_i32.to_be_bytes().to_vec())),
128 ("fee".to_string(), Bytes::from(3000_u32.to_be_bytes().to_vec())),
129 ("ticks/60".to_string(), Bytes::from(400_i128.to_be_bytes().to_vec())),
130 ]
131 .into_iter()
132 .collect::<HashMap<String, Bytes>>()
133 }
134
135 #[tokio::test]
136 async fn test_ramses_try_from() {
137 let snapshot = ComponentWithState {
138 state: ProtocolComponentState {
139 component_id: "State1".to_owned(),
140 attributes: ramses_attributes(),
141 balances: HashMap::new(),
142 },
143 component: ramses_component(),
144 component_tvl: None,
145 entrypoints: Vec::new(),
146 };
147
148 assert_eq!(
149 RamsesV3State::new(
150 100,
151 U256::from(200),
152 3000,
153 60,
154 300,
155 vec![TickInfo::new(60, 400).unwrap()],
156 )
157 .unwrap(),
158 try_decode_snapshot_with_defaults(snapshot)
159 .await
160 .unwrap(),
161 );
162 }
163
164 #[tokio::test]
165 #[rstest]
166 #[case::missing_liquidity("liquidity")]
167 #[case::missing_sqrt_price("sqrt_price_x96")]
168 #[case::missing_tick("tick")]
169 #[case::missing_fee("fee")]
170 #[case::missing_tick_spacing("tick_spacing")]
171 async fn test_ramses_try_from_invalid(#[case] missing_attribute: String) {
172 let mut attributes = ramses_attributes();
173 attributes.remove(&missing_attribute);
174
175 let mut component = ramses_component();
176 component
177 .static_attributes
178 .remove(&missing_attribute);
179
180 let snapshot = ComponentWithState {
181 state: ProtocolComponentState {
182 component_id: "State1".to_owned(),
183 attributes,
184 balances: HashMap::new(),
185 },
186 component,
187 component_tvl: None,
188 entrypoints: Vec::new(),
189 };
190
191 let result = try_decode_snapshot_with_defaults::<RamsesV3State>(snapshot).await;
192
193 assert!(matches!(
194 result.unwrap_err(),
195 InvalidSnapshotError::MissingAttribute(attr) if attr == missing_attribute
196 ));
197 }
198
199 #[tokio::test]
200 async fn test_ramses_try_from_uninitialized() {
201 let mut attributes = ramses_attributes();
204 attributes.remove("ticks/60");
205 attributes.insert("liquidity".to_string(), Bytes::from(vec![0u8]));
206 attributes.insert("sqrt_price_x96".to_string(), Bytes::from(vec![0u8]));
207
208 let snapshot = ComponentWithState {
209 state: ProtocolComponentState {
210 component_id: "State1".to_owned(),
211 attributes,
212 balances: HashMap::new(),
213 },
214 component: ramses_component(),
215 component_tvl: None,
216 entrypoints: Vec::new(),
217 };
218
219 let result = try_decode_snapshot_with_defaults::<RamsesV3State>(snapshot).await;
220
221 assert!(result.is_ok());
222 }
223}