tycho-simulation 0.452.0

Provides tools for interacting with protocol states, calculating spot prices, and quoting token swaps.
Documentation
use std::collections::HashMap;

use tycho_client::feed::synchronizer::ComponentWithState;
use tycho_common::{models::token::Token, Bytes};

use super::{
    all_pairs_state::NativeAllPairsState, client_builder::NativeClientBuilder,
    models::NativePriceData,
};
use crate::{
    protocol::{
        errors::InvalidSnapshotError,
        models::{DecoderContext, TryFromWithBlock},
    },
    rfq::{
        models::{ComponentLayout, QuoteRule, TimestampHeader},
        protocols::component::{decode_all_pairs_component, DecodedAllPairs},
    },
};

impl TryFromWithBlock<ComponentWithState, TimestampHeader> for NativeAllPairsState {
    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 DecodedAllPairs { books, tokens, quote_rule } =
            decode_all_pairs_component::<NativePriceData>(&snapshot, all_tokens)?;
        if quote_rule.is_some_and(|rule| rule != QuoteRule::OncePerVenue) {
            return Err(InvalidSnapshotError::ValueError(
                "Native names no market maker; its quote rule is once_per_venue".into(),
            ));
        }

        let client = NativeClientBuilder::from_env(snapshot.component.chain)
            .map_err(|e| {
                InvalidSnapshotError::ValueError(format!(
                    "Failed to get Native Relay authentication: {e}"
                ))
            })?
            .tokens(tokens.keys().cloned().collect())
            .component_layout(ComponentLayout::AllPairs)
            .build()
            .map_err(|e| {
                InvalidSnapshotError::MissingAttribute(format!("Couldn't create NativeClient: {e}"))
            })?;

        NativeAllPairsState::new(books, tokens, client)
            .map_err(|e| InvalidSnapshotError::ValueError(e.to_string()))
    }
}

#[cfg(test)]
mod tests {
    use std::env;

    use num_bigint::BigUint;
    use tycho_common::simulation::protocol_sim::ProtocolSim;

    use super::*;
    use crate::rfq::protocols::{
        native::models::NativePriceLevel,
        test_utils::{all_pairs_snapshot, decode, usdc, wbtc, weth},
    };

    fn book(base: &Token, quote: &Token, bid: f64, ask: f64) -> NativePriceData {
        NativePriceData {
            base_address: base.address.clone(),
            quote_address: quote.address.clone(),
            minimum_in_base: 0.0,
            minimum_in_quote: 0.0,
            minimum_out_base: 0.0,
            minimum_out_quote: 0.0,
            bids: vec![NativePriceLevel { quantity: 1.5, price: bid }],
            asks: vec![NativePriceLevel { quantity: 2.0, price: ask }],
        }
    }

    fn snapshot() -> (ComponentWithState, HashMap<Bytes, Token>) {
        env::set_var("NATIVE_API_KEY", "test_key");
        let books =
            vec![book(&weth(), &usdc(), 3000.0, 3010.0), book(&wbtc(), &usdc(), 65000.0, 65100.0)];
        all_pairs_snapshot("rfq:native", &[weth(), usdc(), wbtc()], &books)
    }

    #[tokio::test]
    async fn test_decodes_books() {
        let (snapshot, tokens) = snapshot();
        let state = decode::<NativeAllPairsState>(snapshot, &tokens)
            .await
            .unwrap();

        assert_eq!(
            state
                .spot_price(&weth(), &usdc())
                .unwrap(),
            3005.0
        );
        assert_eq!(
            state
                .spot_price(&wbtc(), &usdc())
                .unwrap(),
            65050.0
        );
        let (sell_limit, _) = state
            .get_limits(weth().address, usdc().address)
            .unwrap();
        assert_eq!(sell_limit, BigUint::from(1_500_000_000_000_000_000u64));
    }

    #[tokio::test]
    async fn test_once_per_maker_attribute() {
        let (mut snapshot, tokens) = snapshot();
        snapshot
            .component
            .static_attributes
            .insert(QuoteRule::ATTRIBUTE.to_string(), b"once_per_maker".into());
        let result = decode::<NativeAllPairsState>(snapshot, &tokens).await;
        assert!(
            matches!(result.unwrap_err(), InvalidSnapshotError::ValueError(msg) if msg.contains("names no market maker"))
        );
    }
}