Skip to main content

tycho_simulation/evm/protocol/lunarbase/
decoder.rs

1use std::collections::HashMap;
2
3use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
4use tycho_common::{models::token::Token, Bytes};
5
6use super::state::{Address, LunarBaseTychoState};
7use crate::protocol::{
8    errors::InvalidSnapshotError,
9    models::{DecoderContext, TryFromWithBlock},
10};
11
12mod attrs {
13    pub const ANCHOR_PRICE_X96: &str = "anchor_price_x96";
14    pub const FEE_ASK_X24: &str = "fee_ask_x24";
15    pub const FEE_BID_X24: &str = "fee_bid_x24";
16    pub const LATEST_UPDATE_BLOCK: &str = "latest_update_block";
17    pub const RESERVE_X: &str = "reserve_x";
18    pub const RESERVE_Y: &str = "reserve_y";
19    pub const CONCENTRATION_K: &str = "concentration_k";
20    pub const BLOCK_DELAY: &str = "block_delay";
21    pub const PAUSED: &str = "paused";
22}
23
24impl TryFromWithBlock<ComponentWithState, BlockHeader> for LunarBaseTychoState {
25    type Error = InvalidSnapshotError;
26
27    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 mut state = decode_lunarbase_snapshot(&snapshot)?;
35        state.head_block = block.number;
36        Ok(state)
37    }
38}
39
40#[cfg(test)]
41pub fn encode_state(state: &LunarBaseTychoState) -> HashMap<String, Bytes> {
42    HashMap::from([
43        (attrs::ANCHOR_PRICE_X96.to_owned(), Bytes::from(state.anchor_price_x96)),
44        (attrs::FEE_ASK_X24.to_owned(), Bytes::from(state.fee_ask_x24)),
45        (attrs::FEE_BID_X24.to_owned(), Bytes::from(state.fee_bid_x24)),
46        (attrs::LATEST_UPDATE_BLOCK.to_owned(), Bytes::from(state.latest_update_block)),
47        (attrs::RESERVE_X.to_owned(), Bytes::from(state.reserve_x)),
48        (attrs::RESERVE_Y.to_owned(), Bytes::from(state.reserve_y)),
49        (attrs::CONCENTRATION_K.to_owned(), Bytes::from(state.concentration_k)),
50        (attrs::BLOCK_DELAY.to_owned(), Bytes::from(state.block_delay)),
51        (attrs::PAUSED.to_owned(), Bytes::from([u8::from(state.paused)])),
52    ])
53}
54
55pub fn apply_delta(
56    state: &mut LunarBaseTychoState,
57    updated_attributes: HashMap<String, Bytes>,
58) -> Result<(), InvalidSnapshotError> {
59    for (name, value) in updated_attributes {
60        match name.as_str() {
61            attrs::ANCHOR_PRICE_X96 => state.anchor_price_x96 = u128::from(value),
62            attrs::FEE_ASK_X24 => state.fee_ask_x24 = u32::from(value),
63            attrs::FEE_BID_X24 => state.fee_bid_x24 = u32::from(value),
64            attrs::LATEST_UPDATE_BLOCK => state.latest_update_block = u64::from(value),
65            attrs::RESERVE_X => state.reserve_x = u128::from(value),
66            attrs::RESERVE_Y => state.reserve_y = u128::from(value),
67            attrs::CONCENTRATION_K => state.concentration_k = u32::from(value),
68            attrs::BLOCK_DELAY => state.block_delay = u64::from(value),
69            attrs::PAUSED => state.paused = decode_bool(attrs::PAUSED, &value)?,
70            _ => {}
71        }
72    }
73    Ok(())
74}
75
76pub fn decode_lunarbase_snapshot(
77    snapshot: &ComponentWithState,
78) -> Result<LunarBaseTychoState, InvalidSnapshotError> {
79    let attrs = &snapshot.state.attributes;
80
81    Ok(LunarBaseTychoState {
82        pool: component_pool(snapshot)?,
83        token_x: component_token(snapshot, 0)?,
84        token_y: component_token(snapshot, 1)?,
85        anchor_price_x96: u128::from(required_attr(attrs, attrs::ANCHOR_PRICE_X96)?.clone()),
86        fee_ask_x24: u32::from(required_attr(attrs, attrs::FEE_ASK_X24)?.clone()),
87        fee_bid_x24: u32::from(required_attr(attrs, attrs::FEE_BID_X24)?.clone()),
88        latest_update_block: u64::from(required_attr(attrs, attrs::LATEST_UPDATE_BLOCK)?.clone()),
89        reserve_x: u128::from(required_attr(attrs, attrs::RESERVE_X)?.clone()),
90        reserve_y: u128::from(required_attr(attrs, attrs::RESERVE_Y)?.clone()),
91        concentration_k: u32::from(required_attr(attrs, attrs::CONCENTRATION_K)?.clone()),
92        block_delay: u64::from(required_attr(attrs, attrs::BLOCK_DELAY)?.clone()),
93        paused: decode_bool(attrs::PAUSED, required_attr(attrs, attrs::PAUSED)?)?,
94        head_block: 0,
95    })
96}
97
98fn component_pool(snapshot: &ComponentWithState) -> Result<Address, InvalidSnapshotError> {
99    address_from_component_id(&snapshot.component.id)
100}
101
102fn component_token(
103    snapshot: &ComponentWithState,
104    idx: usize,
105) -> Result<Address, InvalidSnapshotError> {
106    snapshot
107        .component
108        .tokens
109        .get(idx)
110        .map(|token| token.as_ref())
111        .ok_or_else(|| InvalidSnapshotError::ValueError(format!("missing token index {idx}")))
112        .and_then(address_from_bytes)
113}
114
115fn required_attr<'a>(
116    attrs: &'a HashMap<String, Bytes>,
117    name: &'static str,
118) -> Result<&'a Bytes, InvalidSnapshotError> {
119    attrs
120        .get(name)
121        .ok_or_else(|| InvalidSnapshotError::MissingAttribute(name.to_owned()))
122}
123
124fn decode_bool(name: &'static str, value: &Bytes) -> Result<bool, InvalidSnapshotError> {
125    if value.len() != 1 {
126        return Err(invalid_length(name, 1, value.len()));
127    }
128    Ok(value[0] != 0)
129}
130
131fn address_from_bytes(value: &[u8]) -> Result<Address, InvalidSnapshotError> {
132    value.try_into().map_err(|_| {
133        InvalidSnapshotError::ValueError(format!("expected 20-byte address, got {}", value.len()))
134    })
135}
136
137fn address_from_component_id(value: &str) -> Result<Address, InvalidSnapshotError> {
138    let value = value
139        .strip_prefix("0x")
140        .unwrap_or(value);
141    if value.len() != 40 {
142        return Err(InvalidSnapshotError::ValueError(format!(
143            "expected 20-byte hex address component id, got {value}"
144        )));
145    }
146
147    let mut out = [0u8; 20];
148    for (idx, byte) in out.iter_mut().enumerate() {
149        let start = idx * 2;
150        *byte = u8::from_str_radix(&value[start..start + 2], 16).map_err(|err| {
151            InvalidSnapshotError::ValueError(format!("invalid LunarBase component id hex: {err}"))
152        })?;
153    }
154    Ok(out)
155}
156
157fn invalid_length(name: &'static str, expected: usize, actual: usize) -> InvalidSnapshotError {
158    InvalidSnapshotError::ValueError(format!(
159        "attribute {name} has invalid length: expected {expected}, got {actual}"
160    ))
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    fn addr(byte: u8) -> [u8; 20] {
168        [byte; 20]
169    }
170
171    fn state() -> LunarBaseTychoState {
172        LunarBaseTychoState {
173            pool: addr(9),
174            token_x: addr(1),
175            token_y: addr(2),
176            anchor_price_x96: 1u128 << 96,
177            fee_ask_x24: 10,
178            fee_bid_x24: 11,
179            latest_update_block: 100,
180            reserve_x: 1_000_000,
181            reserve_y: 2_000_000,
182            concentration_k: 4096,
183            block_delay: 2,
184            paused: false,
185            head_block: 100,
186        }
187    }
188
189    #[test]
190    fn encodes_full_state_attributes() {
191        let attrs = encode_state(&state());
192
193        assert_eq!(u128::from(attrs[attrs::ANCHOR_PRICE_X96].clone()), 1u128 << 96);
194        assert_eq!(u32::from(attrs[attrs::FEE_ASK_X24].clone()), 10);
195        assert_eq!(u64::from(attrs[attrs::LATEST_UPDATE_BLOCK].clone()), 100);
196        assert!(!decode_bool(attrs::PAUSED, &attrs[attrs::PAUSED]).unwrap());
197    }
198
199    #[test]
200    fn applies_partial_state_updated_delta() {
201        let mut state = state();
202        let updated = HashMap::from([
203            (attrs::ANCHOR_PRICE_X96.to_owned(), Bytes::from(2u128 << 96)),
204            (attrs::FEE_ASK_X24.to_owned(), Bytes::from(20u32)),
205            (attrs::FEE_BID_X24.to_owned(), Bytes::from(21u32)),
206            (attrs::LATEST_UPDATE_BLOCK.to_owned(), Bytes::from(101u64)),
207        ]);
208
209        apply_delta(&mut state, updated).unwrap();
210
211        assert_eq!(state.anchor_price_x96, 2u128 << 96);
212        assert_eq!(state.fee_ask_x24, 20);
213        assert_eq!(state.fee_bid_x24, 21);
214        assert_eq!(state.latest_update_block, 101);
215        assert_eq!(state.reserve_x, 1_000_000);
216    }
217}