use std::collections::HashMap;
use alloy::primitives::U256;
use itertools::Itertools;
use num_traits::Zero;
use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader};
use tycho_common::{models::token::Token, Bytes};
use super::state::RamsesV3State;
use crate::{
evm::protocol::utils::uniswap::tick_list::TickInfo,
protocol::{
errors::InvalidSnapshotError,
models::{DecoderContext, TryFromWithBlock},
},
};
impl TryFromWithBlock<ComponentWithState, BlockHeader> for RamsesV3State {
type Error = InvalidSnapshotError;
async fn try_from_with_header(
snapshot: ComponentWithState,
_block: BlockHeader,
_account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
_all_tokens: &HashMap<Bytes, Token>,
_decoder_context: &DecoderContext,
) -> Result<Self, Self::Error> {
let static_attrs = snapshot.component.static_attributes;
let state_attrs = snapshot.state.attributes;
let fee = u32::from(attribute(&state_attrs, "fee")?.clone());
let liquidity = u128::from(attribute(&state_attrs, "liquidity")?.clone());
let sqrt_price = U256::from_be_slice(attribute(&state_attrs, "sqrt_price_x96")?);
let tick_spacing = u16::from(attribute(&static_attrs, "tick_spacing")?.clone());
let tick = i32::from(attribute(&state_attrs, "tick")?.clone());
let ticks = state_attrs
.iter()
.filter_map(|(key, value)| {
let tick_index = match key
.strip_prefix("ticks/")?
.parse::<i32>()
{
Ok(tick_index) => tick_index,
Err(err) => return Some(Err(InvalidSnapshotError::ValueError(err.to_string()))),
};
let net_liquidity = i128::from(value.clone());
if net_liquidity.is_zero() {
return None;
}
Some(
TickInfo::new(tick_index, net_liquidity)
.map_err(|err| InvalidSnapshotError::ValueError(err.to_string())),
)
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.sorted_unstable_by_key(|tick| tick.index)
.collect();
RamsesV3State::new(liquidity, sqrt_price, fee, tick_spacing, tick, ticks)
.map_err(|err| InvalidSnapshotError::ValueError(err.to_string()))
}
}
fn attribute<'a>(
map: &'a HashMap<String, Bytes>,
key: &str,
) -> Result<&'a Bytes, InvalidSnapshotError> {
map.get(key)
.ok_or_else(|| InvalidSnapshotError::MissingAttribute(key.to_string()))
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use chrono::DateTime;
use rstest::rstest;
use tycho_common::models::{
protocol::{ProtocolComponent, ProtocolComponentState},
Chain, ChangeType,
};
use super::*;
use crate::evm::protocol::test_utils::try_decode_snapshot_with_defaults;
fn ramses_component() -> ProtocolComponent {
let creation_time = DateTime::from_timestamp(1622526000, 0)
.unwrap()
.naive_utc();
let mut static_attributes: HashMap<String, Bytes> = HashMap::new();
static_attributes
.insert("tick_spacing".to_string(), Bytes::from(60_u16.to_be_bytes().to_vec()));
ProtocolComponent {
id: "State1".to_string(),
protocol_system: "ramses_v3".to_string(),
protocol_type_name: "ramses_v3_pool".to_string(),
chain: Chain::Polygon,
tokens: Vec::new(),
contract_addresses: Vec::new(),
static_attributes,
change: ChangeType::Creation,
creation_tx: Bytes::from_str("0x0000").unwrap(),
created_at: creation_time,
}
}
fn ramses_attributes() -> HashMap<String, Bytes> {
vec![
("liquidity".to_string(), Bytes::from(100_u64.to_be_bytes().to_vec())),
("sqrt_price_x96".to_string(), Bytes::from(200_u64.to_be_bytes().to_vec())),
("tick".to_string(), Bytes::from(300_i32.to_be_bytes().to_vec())),
("fee".to_string(), Bytes::from(3000_u32.to_be_bytes().to_vec())),
("ticks/60".to_string(), Bytes::from(400_i128.to_be_bytes().to_vec())),
]
.into_iter()
.collect::<HashMap<String, Bytes>>()
}
#[tokio::test]
async fn test_ramses_try_from() {
let snapshot = ComponentWithState {
state: ProtocolComponentState {
component_id: "State1".to_owned(),
attributes: ramses_attributes(),
balances: HashMap::new(),
},
component: ramses_component(),
component_tvl: None,
entrypoints: Vec::new(),
};
assert_eq!(
RamsesV3State::new(
100,
U256::from(200),
3000,
60,
300,
vec![TickInfo::new(60, 400).unwrap()],
)
.unwrap(),
try_decode_snapshot_with_defaults(snapshot)
.await
.unwrap(),
);
}
#[tokio::test]
#[rstest]
#[case::missing_liquidity("liquidity")]
#[case::missing_sqrt_price("sqrt_price_x96")]
#[case::missing_tick("tick")]
#[case::missing_fee("fee")]
#[case::missing_tick_spacing("tick_spacing")]
async fn test_ramses_try_from_invalid(#[case] missing_attribute: String) {
let mut attributes = ramses_attributes();
attributes.remove(&missing_attribute);
let mut component = ramses_component();
component
.static_attributes
.remove(&missing_attribute);
let snapshot = ComponentWithState {
state: ProtocolComponentState {
component_id: "State1".to_owned(),
attributes,
balances: HashMap::new(),
},
component,
component_tvl: None,
entrypoints: Vec::new(),
};
let result = try_decode_snapshot_with_defaults::<RamsesV3State>(snapshot).await;
assert!(matches!(
result.unwrap_err(),
InvalidSnapshotError::MissingAttribute(attr) if attr == missing_attribute
));
}
#[tokio::test]
async fn test_ramses_try_from_uninitialized() {
let mut attributes = ramses_attributes();
attributes.remove("ticks/60");
attributes.insert("liquidity".to_string(), Bytes::from(vec![0u8]));
attributes.insert("sqrt_price_x96".to_string(), Bytes::from(vec![0u8]));
let snapshot = ComponentWithState {
state: ProtocolComponentState {
component_id: "State1".to_owned(),
attributes,
balances: HashMap::new(),
},
component: ramses_component(),
component_tvl: None,
entrypoints: Vec::new(),
};
let result = try_decode_snapshot_with_defaults::<RamsesV3State>(snapshot).await;
assert!(result.is_ok());
}
}