Skip to main content

tycho_simulation/
serde_helpers.rs

1/// serde functions for handling bytes as hex strings, such as `bytes::Bytes`
2pub mod hex_bytes {
3    use serde::{Deserialize, Deserializer, Serializer};
4
5    /// Serialize a byte vec as a hex string with 0x prefix
6    pub fn serialize<S, T>(x: T, s: S) -> Result<S::Ok, S::Error>
7    where
8        S: Serializer,
9        T: AsRef<[u8]>,
10    {
11        s.serialize_str(&format!("0x{encoded}", encoded = hex::encode(x.as_ref())))
12    }
13
14    /// Deserialize a hex string into a byte vec
15    /// Accepts a hex string with optional 0x prefix
16    pub fn deserialize<'de, T, D>(d: D) -> Result<T, D::Error>
17    where
18        D: Deserializer<'de>,
19        T: From<Vec<u8>>,
20    {
21        let value = String::deserialize(d)?;
22        if let Some(value) = value.strip_prefix("0x") {
23            hex::decode(value)
24        } else {
25            hex::decode(&value)
26        }
27        .map(Into::into)
28        .map_err(|e| serde::de::Error::custom(e.to_string()))
29    }
30}
31
32/// serde functions for handling Option of bytes
33pub mod hex_bytes_option {
34    use serde::{Deserialize, Deserializer, Serializer};
35
36    /// Serialize a byte vec as a Some hex string with 0x prefix
37    pub fn serialize<S, T>(x: &Option<T>, s: S) -> Result<S::Ok, S::Error>
38    where
39        S: Serializer,
40        T: AsRef<[u8]>,
41    {
42        if let Some(x) = x {
43            s.serialize_str(&format!("0x{encoded}", encoded = hex::encode(x.as_ref())))
44        } else {
45            s.serialize_none()
46        }
47    }
48
49    /// Deserialize a hex string into a byte vec or None
50    /// Accepts a hex string with optional 0x prefix
51    pub fn deserialize<'de, T, D>(d: D) -> Result<Option<T>, D::Error>
52    where
53        D: Deserializer<'de>,
54        T: From<Vec<u8>>,
55    {
56        let value: Option<String> = Option::deserialize(d)?;
57
58        match value {
59            Some(val) => {
60                let val = if let Some(stripped) = val.strip_prefix("0x") { stripped } else { &val };
61                hex::decode(val)
62                    .map(Into::into)
63                    .map(Some)
64                    .map_err(|e| serde::de::Error::custom(e.to_string()))
65            }
66            None => Ok(None),
67        }
68    }
69}
70
71/// Serde helpers for `HashMap<String, Box<dyn ProtocolSim>>`.
72///
73/// Some `ProtocolSim` implementations (VM-backed states) return errors from
74/// their `Serialize` impl. This module provides a custom serializer that
75/// gracefully skips those entries instead of failing the entire map.
76pub mod protocol_states {
77    use std::collections::HashMap;
78
79    use serde::{ser::SerializeMap, Deserialize, Deserializer, Serializer};
80    use tracing::{debug, warn};
81    use tycho_common::simulation::protocol_sim::ProtocolSim;
82
83    /// Serializes a map of `ProtocolSim` trait objects, skipping entries
84    /// whose `Serialize` impl returns an error (e.g., VM-backed states).
85    pub fn serialize<S>(
86        states: &HashMap<String, Box<dyn ProtocolSim>>,
87        serializer: S,
88    ) -> Result<S::Ok, S::Error>
89    where
90        S: Serializer,
91    {
92        let mut map = serializer.serialize_map(None)?;
93        let mut skipped = 0u32;
94        for (key, value) in states {
95            match serde_json::to_value(value.as_ref()) {
96                Ok(json_val) => map.serialize_entry(key, &json_val)?,
97                Err(err) => {
98                    debug!(key, %err, "skipping non-serializable ProtocolSim entry");
99                    skipped += 1;
100                }
101            }
102        }
103        if skipped > 0 {
104            warn!(skipped, total = states.len(), "skipped non-serializable ProtocolSim entries");
105        }
106        map.end()
107    }
108
109    /// Deserializes back into the map. Non-serializable states are simply
110    /// absent from the data, so default deserialization works.
111    pub fn deserialize<'de, D>(
112        deserializer: D,
113    ) -> Result<HashMap<String, Box<dyn ProtocolSim>>, D::Error>
114    where
115        D: Deserializer<'de>,
116    {
117        HashMap::<String, Box<dyn ProtocolSim>>::deserialize(deserializer)
118    }
119}
120
121/// Macro to implement error-returning Serialize/Deserialize for protocols
122/// that cannot be serialized (e.g., due to VM state or external SDK dependencies).
123///
124/// # Examples
125/// ```ignore
126/// impl_non_serializable_protocol!(MyProtocolState, "error message");
127/// ```
128#[macro_export]
129macro_rules! impl_non_serializable_protocol {
130    ($type:ty, $msg:expr) => {
131        impl serde::Serialize for $type {
132            fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
133            where
134                S: serde::Serializer,
135            {
136                Err(serde::ser::Error::custom($msg))
137            }
138        }
139
140        impl<'de> serde::Deserialize<'de> for $type {
141            fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
142            where
143                D: serde::Deserializer<'de>,
144            {
145                Err(serde::de::Error::custom($msg))
146            }
147        }
148    };
149}
150
151#[cfg(test)]
152mod tests {
153    use std::collections::HashMap;
154
155    use serde::{Deserialize, Serialize};
156    use serde_json;
157    use tycho_common::simulation::protocol_sim::ProtocolSim;
158
159    use super::*;
160    use crate::protocol::models::Update;
161
162    #[derive(Debug, Serialize, Deserialize)]
163    struct TestStruct {
164        #[serde(with = "hex_bytes")]
165        bytes: Vec<u8>,
166
167        #[serde(with = "hex_bytes_option")]
168        bytes_option: Option<Vec<u8>>,
169    }
170
171    #[test]
172    fn hex_bytes_serialize_deserialize() {
173        let test_struct = TestStruct { bytes: vec![0u8; 10], bytes_option: Some(vec![0u8; 10]) };
174
175        // Serialize to JSON
176        let serialized = serde_json::to_string(&test_struct).unwrap();
177        assert_eq!(
178            serialized,
179            "{\"bytes\":\"0x00000000000000000000\",\"bytes_option\":\"0x00000000000000000000\"}"
180        );
181
182        // Deserialize from JSON
183        let deserialized: TestStruct = serde_json::from_str(&serialized).unwrap();
184        assert_eq!(deserialized.bytes, vec![0u8; 10]);
185        assert_eq!(deserialized.bytes_option, Some(vec![0u8; 10]));
186    }
187
188    #[test]
189    fn hex_bytes_option_none() {
190        let test_struct = TestStruct { bytes: vec![0u8; 10], bytes_option: None };
191
192        // Serialize to JSON
193        let serialized = serde_json::to_string(&test_struct).unwrap();
194        assert_eq!(serialized, "{\"bytes\":\"0x00000000000000000000\",\"bytes_option\":null}");
195
196        // Deserialize from JSON
197        let deserialized: TestStruct = serde_json::from_str(&serialized).unwrap();
198        assert_eq!(deserialized.bytes, vec![0u8; 10]);
199        assert_eq!(deserialized.bytes_option, None);
200    }
201
202    #[cfg(feature = "evm")]
203    #[test]
204    fn update_roundtrip_with_serializable_state() {
205        use alloy::primitives::U256;
206
207        use crate::evm::protocol::uniswap_v2::state::UniswapV2State;
208
209        let mut states: HashMap<String, Box<dyn ProtocolSim>> = HashMap::new();
210        states.insert(
211            "pool_a".to_string(),
212            Box::new(UniswapV2State::new(U256::from(1000), U256::from(2000))),
213        );
214
215        let update = Update::new(12345, states, HashMap::new());
216        let json = serde_json::to_string(&update).unwrap();
217
218        assert!(json.contains("pool_a"));
219
220        let roundtripped: Update = serde_json::from_str(&json).unwrap();
221        assert_eq!(roundtripped.block_number_or_timestamp, 12345);
222        assert_eq!(roundtripped.states.len(), 1);
223        assert!(roundtripped
224            .states
225            .contains_key("pool_a"));
226    }
227
228    #[cfg(feature = "evm")]
229    #[test]
230    fn protocol_states_skips_non_serializable_entries() {
231        use alloy::primitives::U256;
232
233        use crate::evm::protocol::{
234            uniswap_v2::state::UniswapV2State,
235            uniswap_v4::state::{UniswapV4Fees, UniswapV4State},
236        };
237
238        let mut states: HashMap<String, Box<dyn ProtocolSim>> = HashMap::new();
239        states.insert(
240            "serializable".to_string(),
241            Box::new(UniswapV2State::new(U256::from(1000), U256::from(2000))),
242        );
243        states.insert(
244            "non_serializable".to_string(),
245            Box::new(
246                UniswapV4State::new(0, U256::from(1), UniswapV4Fees::new(0, 0, 3000), 0, 1, vec![])
247                    .expect("valid state"),
248            ),
249        );
250
251        let update = Update::new(42, states, HashMap::new());
252        let json = serde_json::to_string(&update).expect("serialization should succeed");
253        let parsed: serde_json::Value = serde_json::from_str(&json).expect("valid JSON");
254
255        let states_map = parsed["states"]
256            .as_object()
257            .expect("states is a map");
258        assert_eq!(states_map.len(), 1, "only the serializable entry should survive");
259        assert!(states_map.contains_key("serializable"));
260        assert!(!states_map.contains_key("non_serializable"));
261    }
262
263    #[cfg(feature = "evm")]
264    #[test]
265    fn protocol_states_serialize_produces_valid_json() {
266        use alloy::primitives::U256;
267
268        use crate::evm::protocol::uniswap_v2::state::UniswapV2State;
269
270        let mut states: HashMap<String, Box<dyn ProtocolSim>> = HashMap::new();
271        states.insert(
272            "pool_x".to_string(),
273            Box::new(UniswapV2State::new(U256::from(100), U256::from(200))),
274        );
275        states.insert(
276            "pool_y".to_string(),
277            Box::new(UniswapV2State::new(U256::from(300), U256::from(400))),
278        );
279
280        #[derive(Serialize)]
281        struct Wrapper {
282            #[serde(with = "protocol_states")]
283            states: HashMap<String, Box<dyn ProtocolSim>>,
284        }
285
286        let wrapper = Wrapper { states };
287        let json = serde_json::to_value(&wrapper).unwrap();
288        let map = json["states"].as_object().unwrap();
289        assert_eq!(map.len(), 2);
290        assert!(map.contains_key("pool_x"));
291        assert!(map.contains_key("pool_y"));
292    }
293
294    #[test]
295    fn update_roundtrip_empty() {
296        let update = Update::new(99999, HashMap::new(), HashMap::new());
297        let json = serde_json::to_string(&update).unwrap();
298        let roundtripped: Update = serde_json::from_str(&json).unwrap();
299        assert_eq!(roundtripped.block_number_or_timestamp, 99999);
300        assert!(roundtripped.states.is_empty());
301        assert!(roundtripped.new_pairs.is_empty());
302    }
303}