use std::{collections::HashMap, env, fmt, time::Duration};
use tokio_stream::Stream;
use tracing::{info, warn};
use tycho_simulation::{
evm::{
engine_db::tycho_db::PreCachedDB,
protocol::{
aerodrome_slipstreams::state::AerodromeSlipstreamsState,
aerodrome_v1::state::AerodromeV1State,
curve::CurveState,
ekubo::state::EkuboState,
ekubo_v3::state::EkuboV3State,
erc4626::state::ERC4626State,
filters::{
balancer_v2_pool_filter, curve_filter, ekubo_v3_extension_filter,
ekubo_v3_extension_filter_with_signed_exclusive_swap, erc4626_filter,
fluid_v1_paused_pools_filter,
},
fluid::FluidV1,
lunarbase::state::LunarBaseState,
pancakeswap_v2::state::PancakeswapV2State,
uniswap_v2::state::UniswapV2State,
uniswap_v3::state::UniswapV3State,
uniswap_v4::state::UniswapV4State,
vm::state::EVMPoolState,
},
stream::ProtocolStreamBuilder,
tycho_models::Chain,
},
price_level_stream::{config::default_served_pamms, stream::PriceLevelStreamBuilder},
protocol::models::Update,
rfq::{
protocols::{
bebop::{client_builder::BebopClientBuilder, state::BebopState},
hashflow::{client_builder::HashflowClientBuilder, state::HashflowState},
},
stream::RFQStreamBuilder,
},
tycho_client::feed::component_tracker::ComponentFilter,
tycho_common::models::token::Token,
tycho_core::Bytes,
};
use super::DataFeedError;
const EXCLUSIVE_PREFIX: &str = "exclusive:";
const EXCLUSIVE_CAPABLE_PROTOCOLS: &[&str] = &["ekubo_v3"];
const PRICE_LEVEL_STREAM_PREFIX: &str = "pricelevelstream:";
const PROPAMM_FALLBACK_PREFIX: &str = "propammfallback:";
const RFQ_PREFIX: &str = "rfq:";
pub const EXCLUDE_PREFIX: &str = "exclude:";
const PRICE_LEVEL_STREAM_CHAIN: Chain = Chain::Ethereum;
pub(crate) fn has_tycho_protocols(protocols: &[String]) -> bool {
protocols.iter().any(|protocol| {
!protocol.starts_with(RFQ_PREFIX) && !protocol.starts_with(PRICE_LEVEL_STREAM_PREFIX)
})
}
pub fn matches_streamed_system(entry: &str, protocol_system: &str) -> bool {
if entry == protocol_system {
return true;
}
match (
entry.strip_prefix(PRICE_LEVEL_STREAM_PREFIX),
protocol_system.strip_prefix(PROPAMM_FALLBACK_PREFIX),
) {
(Some(requested_venue), Some(streamed_venue)) => requested_venue == streamed_venue,
_ => false,
}
}
pub(crate) fn has_rfq_protocols(protocols: &[String]) -> bool {
protocols
.iter()
.any(|protocol| protocol.starts_with(RFQ_PREFIX))
}
#[derive(Debug, thiserror::Error)]
#[error(
"protocol '{requested}' has no exclusive-liquidity variant; '{EXCLUSIVE_PREFIX}' is only \
supported for: {supported}",
supported = EXCLUSIVE_CAPABLE_PROTOCOLS.join(", ")
)]
pub struct UnsupportedExclusiveProtocol {
requested: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProtocolSpec {
pub system: String,
pub exclusive: bool,
}
impl ProtocolSpec {
pub fn public(system: impl Into<String>) -> Self {
Self { system: system.into(), exclusive: false }
}
pub fn parse(entry: &str) -> Result<Self, UnsupportedExclusiveProtocol> {
let Some(system) = entry.strip_prefix(EXCLUSIVE_PREFIX) else {
return Ok(Self::public(entry));
};
if !EXCLUSIVE_CAPABLE_PROTOCOLS.contains(&system) {
return Err(UnsupportedExclusiveProtocol { requested: system.to_string() });
}
Ok(Self { system: system.to_string(), exclusive: true })
}
}
pub fn parse_exclusion(entry: &str) -> Option<Result<String, UnsupportedExclusiveProtocol>> {
let excluded = entry.strip_prefix(EXCLUDE_PREFIX)?;
Some(ProtocolSpec::parse(excluded).map(|protocol| protocol.system))
}
impl fmt::Display for ProtocolSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.exclusive {
write!(f, "{EXCLUSIVE_PREFIX}{}", self.system)
} else {
f.write_str(&self.system)
}
}
}
#[cfg(feature = "test-utils")]
pub fn register_exchanges_for_recording(
builder: ProtocolStreamBuilder,
tvl_filter: ComponentFilter,
entries: &[String],
) -> Result<ProtocolStreamBuilder, String> {
register_exchanges(builder, tvl_filter, entries).map_err(|e| e.to_string())
}
fn parse_protocols(entries: &[String]) -> Result<Vec<ProtocolSpec>, DataFeedError> {
let mut protocols = Vec::with_capacity(entries.len());
for entry in entries {
protocols
.push(ProtocolSpec::parse(entry).map_err(|e| DataFeedError::Config(e.to_string()))?);
}
let mut variants: HashMap<&str, bool> = HashMap::new();
for protocol in &protocols {
if variants
.insert(protocol.system.as_str(), protocol.exclusive)
.is_some_and(|previous| previous != protocol.exclusive)
{
return Err(DataFeedError::Config(format!(
"protocol '{}' requested both with and without the '{EXCLUSIVE_PREFIX}' prefix",
protocol.system
)));
}
}
Ok(protocols)
}
pub(crate) fn register_exchanges(
mut builder: ProtocolStreamBuilder,
tvl_filter: ComponentFilter,
entries: &[String],
) -> Result<ProtocolStreamBuilder, DataFeedError> {
for protocol in parse_protocols(entries)? {
match protocol.system.as_str() {
"uniswap_v2" => {
builder =
builder.exchange::<UniswapV2State>("uniswap_v2", tvl_filter.clone(), None);
}
"sushiswap_v2" => {
builder =
builder.exchange::<UniswapV2State>("sushiswap_v2", tvl_filter.clone(), None);
}
"pancakeswap_v2" => {
builder = builder.exchange::<PancakeswapV2State>(
"pancakeswap_v2",
tvl_filter.clone(),
None,
);
}
"uniswap_v3" => {
builder =
builder.exchange::<UniswapV3State>("uniswap_v3", tvl_filter.clone(), None);
}
"pancakeswap_v3" => {
builder =
builder.exchange::<UniswapV3State>("pancakeswap_v3", tvl_filter.clone(), None);
}
"vm:balancer_v2" => {
builder = builder.exchange::<EVMPoolState<PreCachedDB>>(
"vm:balancer_v2",
tvl_filter.clone(),
Some(balancer_v2_pool_filter),
);
}
"uniswap_v4" => {
builder =
builder.exchange::<UniswapV4State>("uniswap_v4", tvl_filter.clone(), None);
}
"ekubo_v2" => {
builder = builder.exchange::<EkuboState>("ekubo_v2", tvl_filter.clone(), None);
}
"vm:curve" => {
builder = builder.exchange::<CurveState>(
"vm:curve",
tvl_filter.clone(),
Some(curve_filter),
);
}
"uniswap_v4_hooks" => {
builder = builder.exchange::<UniswapV4State>(
"uniswap_v4_hooks",
tvl_filter.clone(),
None,
);
}
"vm:maverick_v2" => {
builder = builder.exchange::<EVMPoolState<PreCachedDB>>(
"vm:maverick_v2",
tvl_filter.clone(),
None,
);
}
"vm:bopamm" => {
builder = builder.exchange::<EVMPoolState<PreCachedDB>>(
"vm:bopamm",
tvl_filter.clone(),
None,
);
}
"vm:fermiswap" => {
builder = builder.exchange::<EVMPoolState<PreCachedDB>>(
"vm:fermiswap",
tvl_filter.clone(),
None,
);
}
"fluid_v1" => {
builder = builder.exchange::<FluidV1>(
"fluid_v1",
tvl_filter.clone(),
Some(fluid_v1_paused_pools_filter),
);
}
"aerodrome_v1" => {
builder =
builder.exchange::<AerodromeV1State>("aerodrome_v1", tvl_filter.clone(), None);
}
"aerodrome_slipstreams" => {
builder = builder.exchange::<AerodromeSlipstreamsState>(
"aerodrome_slipstreams",
tvl_filter.clone(),
None,
);
}
"erc4626" => {
builder = builder.exchange::<ERC4626State>(
"erc4626",
tvl_filter.clone(),
Some(erc4626_filter),
);
}
"velodrome_slipstreams" => {
builder = builder.exchange::<AerodromeSlipstreamsState>(
"velodrome_slipstreams",
tvl_filter.clone(),
None,
);
}
"ekubo_v3" => {
let filter = if protocol.exclusive {
info!("Including exclusive liquidity for ekubo_v3");
ekubo_v3_extension_filter_with_signed_exclusive_swap
} else {
ekubo_v3_extension_filter
};
builder =
builder.exchange::<EkuboV3State>("ekubo_v3", tvl_filter.clone(), Some(filter));
}
"quickswap_v2" => {
builder =
builder.exchange::<UniswapV2State>("quickswap_v2", tvl_filter.clone(), None);
}
"lunarbase" => {
builder = builder.exchange::<LunarBaseState>("lunarbase", tvl_filter.clone(), None);
}
p if p.starts_with(RFQ_PREFIX) || p.starts_with(PRICE_LEVEL_STREAM_PREFIX) => {
continue;
}
_ => {
warn!("Skipping unknown protocol: {}", protocol);
}
}
}
Ok(builder)
}
pub(crate) fn register_rfq(
mut rfq_stream_builder: RFQStreamBuilder,
chain: Chain,
min_tvl: f64,
protocols: &[String],
rfq_tokens: std::collections::HashSet<Bytes>,
) -> Result<RFQStreamBuilder, DataFeedError> {
for protocol in protocols {
match protocol.as_str() {
"rfq:bebop" => {
let key = get_env("BEBOP_KEY")?;
info!("Adding {protocol} RFQ client...");
let bebop_client = BebopClientBuilder::new(chain, key)
.tokens(rfq_tokens.clone())
.tvl_threshold(min_tvl)
.build()
.map_err(|e| DataFeedError::StreamError(e.to_string()))?;
rfq_stream_builder =
rfq_stream_builder.add_client::<BebopState>("bebop", Box::new(bebop_client));
}
"rfq:hashflow" => {
let user = get_env("HASHFLOW_USER")?;
let key = get_env("HASHFLOW_KEY")?;
info!("Adding {protocol} RFQ client...");
let hashflow_client = HashflowClientBuilder::new(chain, user, key)
.tokens(rfq_tokens.clone())
.tvl_threshold(min_tvl)
.poll_time(Duration::from_secs(30))
.build()
.map_err(|e| DataFeedError::StreamError(e.to_string()))?;
rfq_stream_builder = rfq_stream_builder
.add_client::<HashflowState>("hashflow", Box::new(hashflow_client));
}
p if p.starts_with(RFQ_PREFIX) => {
warn!("Skipping unknown RFQ protocol: {}", p);
}
_ => {}
}
}
Ok(rfq_stream_builder)
}
pub(crate) fn open_price_level_stream(
chain: Chain,
protocols: &[String],
tokens: &HashMap<Bytes, Token>,
) -> Result<Option<impl Stream<Item = Update> + Send>, DataFeedError> {
let venues: Vec<&str> = protocols
.iter()
.filter_map(|protocol| protocol.strip_prefix(PRICE_LEVEL_STREAM_PREFIX))
.collect();
if venues.is_empty() {
return Ok(None);
}
if chain != PRICE_LEVEL_STREAM_CHAIN {
return Err(DataFeedError::Config(format!(
"the pAMM price level stream serves {PRICE_LEVEL_STREAM_CHAIN} only, but this feed \
runs on {chain}"
)));
}
let served = default_served_pamms();
let mut builder = PriceLevelStreamBuilder::new().with_tokens(tokens.clone());
for venue in venues {
let Some(config) = served
.iter()
.find(|config| config.protocol == venue)
else {
return Err(DataFeedError::Config(format!(
"unknown pAMM '{venue}' for the price level stream; served venues are: {}",
served
.iter()
.map(|config| config.protocol.as_str())
.collect::<Vec<_>>()
.join(", ")
)));
};
info!("Adding {PRICE_LEVEL_STREAM_PREFIX}{venue} price level venue...");
builder = builder.add_pamm(config.clone());
}
Ok(Some(builder.build()))
}
#[cfg(feature = "test-utils")]
pub fn open_price_level_stream_for_recording(
chain: Chain,
protocols: &[String],
tokens: &HashMap<Bytes, Token>,
) -> Result<Option<impl Stream<Item = Update> + Send>, String> {
open_price_level_stream(chain, protocols, tokens).map_err(|e| e.to_string())
}
fn get_env(var: &str) -> Result<String, DataFeedError> {
env::var(var).map_err(|_| DataFeedError::Config(format!("{} env var not set", var)))
}
#[cfg(test)]
mod tests {
use tycho_simulation::price_level_stream::config::PRICE_LEVEL_STREAM_FAMILY;
use super::*;
fn register(entries: &[&str]) -> Result<ProtocolStreamBuilder, DataFeedError> {
register_exchanges(
ProtocolStreamBuilder::new("localhost:0", Chain::Ethereum),
ComponentFilter::with_tvl_range(1.0, 10.0),
&entries
.iter()
.map(|entry| (*entry).to_string())
.collect::<Vec<_>>(),
)
}
fn price_level_stream(
chain: Chain,
entries: &[&str],
) -> Result<Option<impl Stream<Item = Update> + Send>, DataFeedError> {
open_price_level_stream(
chain,
&entries
.iter()
.map(|entry| (*entry).to_string())
.collect::<Vec<_>>(),
&HashMap::new(),
)
}
#[test]
fn test_parse_plain_protocol() {
let protocol = ProtocolSpec::parse("uniswap_v3").unwrap();
assert_eq!(protocol, ProtocolSpec { system: "uniswap_v3".to_string(), exclusive: false });
}
#[test]
fn test_parse_exclusive_protocol() {
let protocol = ProtocolSpec::parse("exclusive:ekubo_v3").unwrap();
assert_eq!(protocol, ProtocolSpec { system: "ekubo_v3".to_string(), exclusive: true });
}
#[test]
fn test_parse_exclusive_unsupported_protocol() {
let err = ProtocolSpec::parse("exclusive:uniswap_v3").unwrap_err();
assert!(err
.to_string()
.contains("has no exclusive-liquidity variant"));
}
#[test]
fn test_parse_exclusive_without_protocol() {
assert!(ProtocolSpec::parse("exclusive:").is_err());
}
#[test]
fn test_parse_leaves_other_prefixes_intact() {
assert_eq!(
ProtocolSpec::parse("rfq:bebop").unwrap(),
ProtocolSpec { system: "rfq:bebop".to_string(), exclusive: false }
);
assert_eq!(
ProtocolSpec::parse("vm:curve").unwrap(),
ProtocolSpec { system: "vm:curve".to_string(), exclusive: false }
);
}
#[test]
fn test_parse_exclusion() {
assert_eq!(
parse_exclusion("exclude:vm:fermiswap")
.unwrap()
.unwrap(),
"vm:fermiswap"
);
}
#[test]
fn test_parse_exclusion_strips_the_exclusive_prefix() {
assert_eq!(
parse_exclusion("exclude:exclusive:ekubo_v3")
.unwrap()
.unwrap(),
"ekubo_v3"
);
}
#[test]
fn test_parse_exclusion_rejects_unsupported_exclusive() {
assert!(parse_exclusion("exclude:exclusive:uniswap_v3")
.unwrap()
.is_err());
}
#[test]
fn test_parse_exclusion_without_protocol() {
assert_eq!(
parse_exclusion("exclude:")
.unwrap()
.unwrap(),
""
);
}
#[test]
fn test_parse_exclusion_of_a_plain_entry() {
assert!(parse_exclusion("uniswap_v3").is_none());
}
#[test]
fn test_display_round_trips() {
for entry in ["uniswap_v3", "exclusive:ekubo_v3", "rfq:bebop", "vm:curve"] {
let protocol = ProtocolSpec::parse(entry).unwrap();
assert_eq!(protocol.to_string(), entry);
assert_eq!(ProtocolSpec::parse(&protocol.to_string()).unwrap(), protocol);
}
}
#[test]
fn test_register_exchanges_accepts_exclusive_ekubo_v3() {
assert!(register(&["uniswap_v3", "exclusive:ekubo_v3"]).is_ok());
}
#[test]
fn test_register_exchanges_rejects_unsupported_exclusive() {
let Err(err) = register(&["exclusive:uniswap_v3"]) else {
panic!("expected `exclusive:uniswap_v3` to be rejected");
};
assert!(matches!(err, DataFeedError::Config(_)), "expected a config error, got {err:?}");
}
#[test]
fn test_register_exchanges_skips_unknown_protocol() {
assert!(register(&["not_a_protocol"]).is_ok());
}
#[test]
fn test_register_exchanges_rejects_conflicting_variants() {
for protocols in [["ekubo_v3", "exclusive:ekubo_v3"], ["exclusive:ekubo_v3", "ekubo_v3"]] {
let Err(err) = register(&protocols) else {
panic!("expected {protocols:?} to be rejected");
};
assert!(
err.to_string()
.contains("both with and without"),
"unexpected error for {protocols:?}: {err}"
);
}
}
#[test]
fn test_register_exchanges_allows_repeated_protocol() {
assert!(register(&["uniswap_v3", "uniswap_v3"]).is_ok());
}
#[test]
fn test_price_level_stream_prefix_matches_family() {
assert_eq!(PRICE_LEVEL_STREAM_PREFIX, format!("{PRICE_LEVEL_STREAM_FAMILY}:"));
}
#[test]
fn test_matches_streamed_system() {
assert!(matches_streamed_system("uniswap_v3", "uniswap_v3"));
assert!(matches_streamed_system(
"pricelevelstream:fermiswap",
"pricelevelstream:fermiswap"
));
assert!(matches_streamed_system("pricelevelstream:fermiswap", "propammfallback:fermiswap"));
}
#[test]
fn test_matches_streamed_system_rejects_another_venue() {
assert!(!matches_streamed_system("pricelevelstream:fermiswap", "propammfallback:kipseli"));
assert!(!matches_streamed_system("pricelevelstream:fermiswap", "vm:fermiswap"));
assert!(!matches_streamed_system("uniswap_v3", "propammfallback:fermiswap"));
assert!(!matches_streamed_system("vm:fermiswap", "propammfallback:fermiswap"));
}
#[test]
fn test_has_tycho_protocols() {
assert!(has_tycho_protocols(&["uniswap_v3".to_string()]));
assert!(has_tycho_protocols(&["rfq:bebop".to_string(), "uniswap_v3".to_string()]));
assert!(!has_tycho_protocols(&[
"rfq:bebop".to_string(),
"pricelevelstream:fermiswap".to_string(),
]));
assert!(!has_tycho_protocols(&[]));
}
#[test]
fn test_register_exchanges_skips_price_level_entries() {
assert!(register(&["uniswap_v3", "pricelevelstream:fermiswap"]).is_ok());
}
#[test]
fn test_open_price_level_stream_without_entries() {
let Ok(None) = price_level_stream(Chain::Ethereum, &["uniswap_v3", "rfq:bebop"]) else {
panic!("expected no price level stream without a `pricelevelstream:` entry");
};
}
#[test]
fn test_open_price_level_stream_served_venue() {
let Ok(Some(_)) = price_level_stream(Chain::Ethereum, &["pricelevelstream:fermiswap"])
else {
panic!("expected fermiswap to be served");
};
}
#[test]
fn test_open_price_level_stream_several_venues() {
let Ok(Some(_)) = price_level_stream(
Chain::Ethereum,
&["pricelevelstream:fermiswap", "pricelevelstream:kipseli"],
) else {
panic!("expected both venues to be served");
};
}
#[test]
fn test_open_price_level_stream_unknown_venue() {
for entries in [
vec!["pricelevelstream:nope"],
vec!["pricelevelstream:fermiswap", "pricelevelstream:nope"],
] {
let Err(err) = price_level_stream(Chain::Ethereum, &entries) else {
panic!("expected an unserved venue to be rejected in {entries:?}");
};
assert!(
err.to_string()
.contains("unknown pAMM 'nope'"),
"got {err}"
);
}
}
#[test]
fn test_open_price_level_stream_without_entries_off_ethereum() {
let Ok(None) = price_level_stream(Chain::Base, &["uniswap_v3"]) else {
panic!("expected chains without a `pricelevelstream:` entry to be left alone");
};
}
#[test]
fn test_open_price_level_stream_other_chain() {
let Err(err) = price_level_stream(Chain::Base, &["pricelevelstream:fermiswap"]) else {
panic!("expected the price level stream to be rejected off Ethereum");
};
assert!(
err.to_string()
.contains("serves ethereum only"),
"got {err}"
);
}
}