tycho_simulation/rfq/protocols/metric/
decoder.rs1use std::collections::{HashMap, HashSet};
2
3use tycho_client::feed::synchronizer::ComponentWithState;
4use tycho_common::{models::token::Token, Bytes};
5
6use super::{
7 client_builder::MetricClientBuilder,
8 models::{MetricBidAskResponse, MetricDepth, MetricMetadata},
9 state::MetricState,
10};
11use crate::{
12 protocol::{
13 errors::InvalidSnapshotError,
14 models::{DecoderContext, TryFromWithBlock},
15 },
16 rfq::models::TimestampHeader,
17};
18
19impl TryFromWithBlock<ComponentWithState, TimestampHeader> for MetricState {
20 type Error = InvalidSnapshotError;
21
22 async fn try_from_with_header(
23 snapshot: ComponentWithState,
24 _timestamp_header: TimestampHeader,
25 _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
26 all_tokens: &HashMap<Bytes, Token>,
27 _decoder_context: &DecoderContext,
28 ) -> Result<Self, Self::Error> {
29 if snapshot.component.tokens.len() != 2 {
30 return Err(InvalidSnapshotError::ValueError(
31 "Metric component must have token0 and token1".to_string(),
32 ));
33 }
34
35 let token0_address = &snapshot.component.tokens[0];
36 let token1_address = &snapshot.component.tokens[1];
37 let token0 = all_tokens
38 .get(token0_address)
39 .ok_or_else(|| {
40 InvalidSnapshotError::ValueError(format!(
41 "Metric token0 not found: {token0_address}"
42 ))
43 })?
44 .clone();
45 let token1 = all_tokens
46 .get(token1_address)
47 .ok_or_else(|| {
48 InvalidSnapshotError::ValueError(format!(
49 "Metric token1 not found: {token1_address}"
50 ))
51 })?
52 .clone();
53 let pool_address = snapshot
54 .component
55 .id
56 .parse::<Bytes>()
57 .map_err(|_| {
58 InvalidSnapshotError::ValueError(format!(
59 "Metric component id is not a pool address: {}",
60 snapshot.component.id
61 ))
62 })?;
63
64 let attrs = snapshot.state.attributes;
66 let metadata = MetricMetadata {
67 pool_address,
68 token0: token0_address.clone(),
69 token1: token1_address.clone(),
70 };
71 let bid_ask = MetricBidAskResponse {
72 bid_adj: read_string_attr(&attrs, "bid_adj")?,
73 ask_adj: read_string_attr(&attrs, "ask_adj")?,
74 quote_available: true,
75 total_token0_available: read_string_attr(&attrs, "total_token0_available")?,
76 total_token1_available: read_string_attr(&attrs, "total_token1_available")?,
77 latest_block: read_u64_attr(&attrs, "latest_block")?,
78 depth: read_optional_depth_attr(&attrs, "depth")?,
79 };
80
81 let client = MetricClientBuilder::new(snapshot.component.chain)
82 .tokens(HashSet::from([token0_address.clone(), token1_address.clone()]))
83 .build()
84 .map_err(|e| {
85 InvalidSnapshotError::ValueError(format!("Couldn't create MetricClient: {e}"))
86 })?;
87
88 Ok(MetricState::new(token0, token1, metadata, bid_ask, client))
89 }
90}
91
92fn read_string_attr(
93 attrs: &HashMap<String, Bytes>,
94 name: &str,
95) -> Result<String, InvalidSnapshotError> {
96 let bytes = attrs.get(name).ok_or_else(|| {
97 InvalidSnapshotError::MissingAttribute(format!("{name} attribute not found"))
98 })?;
99 String::from_utf8(bytes.to_vec())
100 .map_err(|_| InvalidSnapshotError::ValueError(format!("Invalid {name} encoding")))
101}
102
103fn read_u64_attr(attrs: &HashMap<String, Bytes>, name: &str) -> Result<u64, InvalidSnapshotError> {
104 read_string_attr(attrs, name)?
105 .parse()
106 .map_err(|_| InvalidSnapshotError::ValueError(format!("Invalid {name} integer")))
107}
108
109fn read_optional_depth_attr(
110 attrs: &HashMap<String, Bytes>,
111 name: &str,
112) -> Result<MetricDepth, InvalidSnapshotError> {
113 match attrs.get(name) {
114 Some(bytes) => serde_json::from_slice(bytes)
115 .map_err(|e| InvalidSnapshotError::ValueError(format!("Invalid {name} JSON: {e}"))),
116 None => Ok(MetricDepth::default()),
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use std::str::FromStr;
123
124 use tycho_common::models::{
125 protocol::{ProtocolComponent, ProtocolComponentState},
126 Chain as ModelChain, ChangeType,
127 };
128
129 use super::*;
130
131 fn weth() -> Token {
132 Token::new(
133 &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
134 "WETH",
135 18,
136 0,
137 &[Some(2300)],
138 ModelChain::Ethereum,
139 100,
140 )
141 }
142
143 fn usdc() -> Token {
144 Token::new(
145 &Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
146 "USDC",
147 6,
148 0,
149 &[Some(1)],
150 ModelChain::Ethereum,
151 100,
152 )
153 }
154
155 fn create_snapshot() -> (ComponentWithState, HashMap<Bytes, Token>) {
156 let weth = weth();
157 let usdc = usdc();
158 let mut tokens = HashMap::new();
159 tokens.insert(weth.address.clone(), weth.clone());
160 tokens.insert(usdc.address.clone(), usdc.clone());
161
162 let mut attrs = HashMap::new();
163 attrs.insert(
164 "bid_adj".to_string(),
165 "55340232221128654848000"
166 .as_bytes()
167 .to_vec()
168 .into(),
169 );
170 attrs.insert(
171 "ask_adj".to_string(),
172 "55524699661865750400000"
173 .as_bytes()
174 .to_vec()
175 .into(),
176 );
177 attrs.insert(
178 "total_token0_available".to_string(),
179 "10000000000000000000"
180 .as_bytes()
181 .to_vec()
182 .into(),
183 );
184 attrs
185 .insert("total_token1_available".to_string(), "30000000000".as_bytes().to_vec().into());
186 attrs.insert("latest_block".to_string(), "100".as_bytes().to_vec().into());
187 attrs.insert("depth".to_string(), r#"{"asks":[],"bids":[]}"#.as_bytes().to_vec().into());
188
189 let pool_address = Bytes::from_str("0xbF48bCf474d57fF82A3215319229e0DE1476A557").unwrap();
190 let snapshot = ComponentWithState {
191 state: ProtocolComponentState {
192 attributes: attrs,
193 component_id: pool_address.to_string(),
194 balances: HashMap::new(),
195 },
196 component: ProtocolComponent {
197 id: pool_address.to_string(),
198 protocol_system: "rfq:metric".to_string(),
199 protocol_type_name: "metric_pool".to_string(),
200 chain: ModelChain::Ethereum,
201 tokens: vec![weth.address.clone(), usdc.address.clone()],
202 contract_addresses: Vec::new(),
203 static_attributes: HashMap::new(),
204 change: ChangeType::Creation,
205 creation_tx: Bytes::default(),
206 created_at: chrono::NaiveDateTime::default(),
207 },
208 component_tvl: None,
209 entrypoints: Vec::new(),
210 };
211
212 (snapshot, tokens)
213 }
214
215 #[tokio::test]
216 async fn test_try_from_with_header() {
217 let (snapshot, tokens) = create_snapshot();
218 let state = MetricState::try_from_with_header(
219 snapshot,
220 TimestampHeader { timestamp: 1_700_000_000 },
221 &HashMap::new(),
222 &tokens,
223 &DecoderContext::new(),
224 )
225 .await
226 .expect("decode metric state");
227
228 assert_eq!(state.base_token.symbol, "WETH");
229 assert_eq!(state.quote_token.symbol, "USDC");
230 assert_eq!(state.bid_ask.latest_block, 100);
231 }
232
233 #[tokio::test]
234 async fn test_try_from_missing_attribute() {
235 let (mut snapshot, tokens) = create_snapshot();
236 snapshot
237 .state
238 .attributes
239 .remove("bid_adj");
240
241 let result = MetricState::try_from_with_header(
242 snapshot,
243 TimestampHeader::default(),
244 &HashMap::new(),
245 &tokens,
246 &DecoderContext::new(),
247 )
248 .await;
249
250 assert!(result.is_err());
251 }
252}