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 tvl_fiat: None,
71 };
72 let bid_ask = MetricBidAskResponse {
73 bid_adj: read_biguint_attr(&attrs, "bid_adj")?,
74 ask_adj: read_biguint_attr(&attrs, "ask_adj")?,
75 total_token0_available: Some(read_biguint_attr(&attrs, "total_token0_available")?),
76 total_token1_available: Some(read_biguint_attr(&attrs, "total_token1_available")?),
77 server_ts: read_u64_attr(&attrs, "server_ts")?,
78 price_provider_status: None,
81 depth: read_optional_depth_attr(&attrs, "depth")?,
82 };
83
84 let client = MetricClientBuilder::new(snapshot.component.chain)
85 .tokens(HashSet::from([token0_address.clone(), token1_address.clone()]))
86 .build()
87 .map_err(|e| {
88 InvalidSnapshotError::ValueError(format!("Couldn't create MetricClient: {e}"))
89 })?;
90
91 Ok(MetricState::new(token0, token1, metadata, bid_ask, client))
92 }
93}
94
95fn read_string_attr(
96 attrs: &HashMap<String, Bytes>,
97 name: &str,
98) -> Result<String, InvalidSnapshotError> {
99 let bytes = attrs.get(name).ok_or_else(|| {
100 InvalidSnapshotError::MissingAttribute(format!("{name} attribute not found"))
101 })?;
102 String::from_utf8(bytes.to_vec())
103 .map_err(|_| InvalidSnapshotError::ValueError(format!("Invalid {name} encoding")))
104}
105
106fn read_biguint_attr(
107 attrs: &HashMap<String, Bytes>,
108 name: &str,
109) -> Result<num_bigint::BigUint, InvalidSnapshotError> {
110 read_string_attr(attrs, name)?
111 .parse()
112 .map_err(|_| InvalidSnapshotError::ValueError(format!("Invalid {name} integer")))
113}
114
115fn read_u64_attr(attrs: &HashMap<String, Bytes>, name: &str) -> Result<u64, InvalidSnapshotError> {
116 read_string_attr(attrs, name)?
117 .parse()
118 .map_err(|_| InvalidSnapshotError::ValueError(format!("Invalid {name} integer")))
119}
120
121fn read_optional_depth_attr(
122 attrs: &HashMap<String, Bytes>,
123 name: &str,
124) -> Result<MetricDepth, InvalidSnapshotError> {
125 match attrs.get(name) {
126 Some(bytes) => serde_json::from_slice(bytes)
127 .map_err(|e| InvalidSnapshotError::ValueError(format!("Invalid {name} JSON: {e}"))),
128 None => Ok(MetricDepth::default()),
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use std::str::FromStr;
135
136 use tycho_common::models::{
137 protocol::{ProtocolComponent, ProtocolComponentState},
138 Chain as ModelChain, ChangeType,
139 };
140
141 use super::*;
142
143 fn weth() -> Token {
144 Token::new(
145 &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
146 "WETH",
147 18,
148 0,
149 &[Some(2300)],
150 ModelChain::Ethereum,
151 100,
152 )
153 }
154
155 fn usdc() -> Token {
156 Token::new(
157 &Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
158 "USDC",
159 6,
160 0,
161 &[Some(1)],
162 ModelChain::Ethereum,
163 100,
164 )
165 }
166
167 fn create_snapshot() -> (ComponentWithState, HashMap<Bytes, Token>) {
168 let weth = weth();
169 let usdc = usdc();
170 let mut tokens = HashMap::new();
171 tokens.insert(weth.address.clone(), weth.clone());
172 tokens.insert(usdc.address.clone(), usdc.clone());
173
174 let mut attrs = HashMap::new();
175 attrs.insert(
176 "bid_adj".to_string(),
177 "55340232221128654848000"
178 .as_bytes()
179 .to_vec()
180 .into(),
181 );
182 attrs.insert(
183 "ask_adj".to_string(),
184 "55524699661865750400000"
185 .as_bytes()
186 .to_vec()
187 .into(),
188 );
189 attrs.insert(
190 "total_token0_available".to_string(),
191 "10000000000000000000"
192 .as_bytes()
193 .to_vec()
194 .into(),
195 );
196 attrs
197 .insert("total_token1_available".to_string(), "30000000000".as_bytes().to_vec().into());
198 attrs.insert("server_ts".to_string(), "100".as_bytes().to_vec().into());
199 attrs.insert("depth".to_string(), r#"{"asks":[],"bids":[]}"#.as_bytes().to_vec().into());
200
201 let pool_address = Bytes::from_str("0xbF48bCf474d57fF82A3215319229e0DE1476A557").unwrap();
202 let snapshot = ComponentWithState {
203 state: ProtocolComponentState {
204 attributes: attrs,
205 component_id: pool_address.to_string(),
206 balances: HashMap::new(),
207 },
208 component: ProtocolComponent {
209 id: pool_address.to_string(),
210 protocol_system: "rfq:metric".to_string(),
211 protocol_type_name: "metric_pool".to_string(),
212 chain: ModelChain::Ethereum,
213 tokens: vec![weth.address.clone(), usdc.address.clone()],
214 contract_addresses: Vec::new(),
215 static_attributes: HashMap::new(),
216 change: ChangeType::Creation,
217 creation_tx: Bytes::default(),
218 created_at: chrono::NaiveDateTime::default(),
219 },
220 component_tvl: None,
221 entrypoints: Vec::new(),
222 };
223
224 (snapshot, tokens)
225 }
226
227 #[tokio::test]
228 async fn test_try_from_with_header() {
229 let (snapshot, tokens) = create_snapshot();
230 let state = MetricState::try_from_with_header(
231 snapshot,
232 TimestampHeader { timestamp: 1_700_000_000 },
233 &HashMap::new(),
234 &tokens,
235 &DecoderContext::new(),
236 )
237 .await
238 .expect("decode metric state");
239
240 assert_eq!(state.base_token.symbol, "WETH");
241 assert_eq!(state.quote_token.symbol, "USDC");
242 assert_eq!(state.bid_ask.server_ts, 100);
243 }
244
245 #[tokio::test]
246 async fn test_try_from_missing_attribute() {
247 let (mut snapshot, tokens) = create_snapshot();
248 snapshot
249 .state
250 .attributes
251 .remove("bid_adj");
252
253 let result = MetricState::try_from_with_header(
254 snapshot,
255 TimestampHeader::default(),
256 &HashMap::new(),
257 &tokens,
258 &DecoderContext::new(),
259 )
260 .await;
261
262 assert!(result.is_err());
263 }
264}