Skip to main content

tycho_simulation/rfq/protocols/metric/
decoder.rs

1use 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        // RFQ snapshots do not carry balances; all Metric pricing data is stored as attributes.
65        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}