use std::collections::{HashMap, HashSet};
use tycho_client::feed::synchronizer::ComponentWithState;
use tycho_common::{models::token::Token, Bytes};
use super::{client_builder::NativeClientBuilder, models::NativePriceData, state::NativeState};
use crate::{
protocol::{
errors::InvalidSnapshotError,
models::{DecoderContext, TryFromWithBlock},
},
rfq::models::TimestampHeader,
};
impl TryFromWithBlock<ComponentWithState, TimestampHeader> for NativeState {
type Error = InvalidSnapshotError;
async fn try_from_with_header(
snapshot: ComponentWithState,
_timestamp_header: TimestampHeader,
_account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
all_tokens: &HashMap<Bytes, Token>,
_decoder_context: &DecoderContext,
) -> Result<Self, Self::Error> {
let state_attrs = snapshot.state.attributes;
if snapshot.component.tokens.len() != 2 {
return Err(InvalidSnapshotError::ValueError(
"Component must have 2 tokens (base and quote)".to_string(),
));
}
let base_token_address = &snapshot.component.tokens[0];
let quote_token_address = &snapshot.component.tokens[1];
let base_token = all_tokens
.get(base_token_address)
.ok_or_else(|| {
InvalidSnapshotError::ValueError(format!(
"Base token not found: {base_token_address}"
))
})?
.clone();
let quote_token = all_tokens
.get(quote_token_address)
.ok_or_else(|| {
InvalidSnapshotError::ValueError(format!(
"Quote token not found: {quote_token_address}"
))
})?
.clone();
let book_data = state_attrs
.get("book")
.ok_or_else(|| InvalidSnapshotError::MissingAttribute("book".to_string()))?;
let book: NativePriceData = serde_json::from_slice(book_data)
.map_err(|e| InvalidSnapshotError::ValueError(format!("Invalid book JSON: {e}")))?;
let client_builder =
NativeClientBuilder::from_env(snapshot.component.chain).map_err(|e| {
InvalidSnapshotError::ValueError(format!(
"Failed to get Native Relay authentication: {e}"
))
})?;
let client = client_builder
.tokens(HashSet::from([base_token.address.clone(), quote_token.address.clone()]))
.build()
.map_err(|e| {
InvalidSnapshotError::MissingAttribute(format!("Couldn't create NativeClient: {e}"))
})?;
NativeState::new(base_token, quote_token, book, client)
.map_err(|e| InvalidSnapshotError::ValueError(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, env};
use tycho_common::models::{
protocol::{ProtocolComponent, ProtocolComponentState},
Chain, ChangeType,
};
use super::*;
use crate::rfq::protocols::native::models::NativePriceLevel;
fn weth() -> Token {
Token::new(
&hex::decode("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")
.unwrap()
.into(),
"WETH",
18,
0,
&[Some(10_000)],
Chain::Ethereum,
100,
)
}
fn usdc() -> Token {
Token::new(
&hex::decode("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
.unwrap()
.into(),
"USDC",
6,
0,
&[Some(10_000)],
Chain::Ethereum,
100,
)
}
fn create_test_book() -> NativePriceData {
NativePriceData {
base_address: weth().address,
quote_address: usdc().address,
minimum_in_base: 0.0,
minimum_in_quote: 0.0,
minimum_out_base: 0.0,
minimum_out_quote: 0.0,
bids: vec![NativePriceLevel { price: 3000.0, quantity: 1.5 }],
asks: vec![NativePriceLevel { price: 3001.0, quantity: 2.0 }],
}
}
fn create_test_snapshot() -> (ComponentWithState, HashMap<Bytes, Token>) {
let weth_token = weth();
let usdc_token = usdc();
let book = create_test_book();
let mut tokens = HashMap::new();
tokens.insert(weth_token.address.clone(), weth_token.clone());
tokens.insert(usdc_token.address.clone(), usdc_token.clone());
let mut state_attributes = HashMap::new();
let book_json = serde_json::to_vec(&book).expect("Failed to serialize book");
state_attributes.insert("book".to_string(), book_json.into());
let snapshot = ComponentWithState {
state: ProtocolComponentState {
attributes: state_attributes,
component_id: "native_market_1".to_string(),
balances: HashMap::new(),
},
component: ProtocolComponent {
id: "native_market_1".to_string(),
protocol_system: "rfq:native".to_string(),
protocol_type_name: "native_relay_pool".to_string(),
chain: Chain::Ethereum,
tokens: vec![weth_token.address.clone(), usdc_token.address.clone()],
contract_addresses: Vec::new(),
static_attributes: HashMap::new(),
change: ChangeType::Creation,
creation_tx: Bytes::default(),
created_at: chrono::NaiveDateTime::default(),
},
component_tvl: Some(4500.0),
entrypoints: Vec::new(),
};
(snapshot, tokens)
}
#[tokio::test]
async fn test_try_from_with_header() {
env::set_var("NATIVE_API_KEY", "test-api-key");
let (snapshot, tokens) = create_test_snapshot();
let result = NativeState::try_from_with_header(
snapshot,
TimestampHeader { timestamp: 1703097600u64 },
&HashMap::new(),
&tokens,
&DecoderContext::new(),
)
.await
.expect("create state from snapshot");
assert_eq!(result.base_token.symbol, "WETH");
assert_eq!(result.quote_token.symbol, "USDC");
assert_eq!(result.book.bids.len(), 1);
assert_eq!(result.book.asks.len(), 1);
assert_eq!(result.book.bids[0].price, 3000.0);
assert_eq!(result.book.bids[0].quantity, 1.5);
}
#[tokio::test]
async fn test_try_from_missing_book() {
let (mut snapshot, tokens) = create_test_snapshot();
snapshot.state.attributes.remove("book");
let result = NativeState::try_from_with_header(
snapshot,
TimestampHeader::default(),
&HashMap::new(),
&tokens,
&DecoderContext::new(),
)
.await;
assert!(matches!(
result.unwrap_err(),
InvalidSnapshotError::MissingAttribute(attribute) if attribute == "book"
));
}
#[tokio::test]
async fn test_try_from_missing_token() {
let (mut snapshot, tokens) = create_test_snapshot();
snapshot.component.tokens.pop();
let result = NativeState::try_from_with_header(
snapshot,
TimestampHeader::default(),
&HashMap::new(),
&tokens,
&DecoderContext::new(),
)
.await;
assert!(matches!(result.unwrap_err(), InvalidSnapshotError::ValueError(_)));
}
}