tycho_simulation/evm/protocol/lunarbase/
decoder.rs1use 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}