1use std::{any::Any, collections::HashMap, fmt, sync::Arc};
2
3use async_trait::async_trait;
4use num_bigint::BigUint;
5use serde::{Deserialize, Serialize};
6use tracing::{debug, warn};
7use tycho_common::{
8 dto::ProtocolStateDelta,
9 models::{protocol::GetAmountOutParams, token::Token},
10 simulation::{
11 errors::{SimulationError, TransitionError},
12 indicatively_priced::{IndicativelyPriced, SignedQuote},
13 protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
14 },
15 Bytes,
16};
17
18use crate::rfq::protocols::native::{
19 client::NativeClient, models::NativePriceData, state::NativeState,
20};
21
22#[derive(Clone, Serialize, Deserialize)]
27pub struct NativeAllPairsState {
28 states: Arc<Vec<NativeState>>,
29 directions: Arc<Vec<((Bytes, Bytes), usize)>>,
32 used: bool,
34}
35
36impl fmt::Debug for NativeAllPairsState {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 f.debug_struct("NativeAllPairsState")
39 .field("books", &self.states.len())
40 .field("used", &self.used)
41 .finish_non_exhaustive()
42 }
43}
44
45impl NativeAllPairsState {
46 pub fn new(
49 books: Vec<NativePriceData>,
50 tokens: HashMap<Bytes, Token>,
51 client: NativeClient,
52 ) -> Self {
53 let mut states = Vec::with_capacity(books.len());
54 for book in books {
55 let (base_address, quote_address) =
56 (book.base_address.clone(), book.quote_address.clone());
57 let (Some(base_token), Some(quote_token)) =
58 (tokens.get(&base_address), tokens.get("e_address))
59 else {
60 debug!(
61 "Skipping Native book {base_address}/{quote_address}: a token is missing from \
62 the state."
63 );
64 continue;
65 };
66 match NativeState::new(base_token.clone(), quote_token.clone(), book, client.clone()) {
67 Ok(state) => states.push(state),
68 Err(e) => warn!(
69 "Native book {base_address}/{quote_address} is invalid: {e}. Skipping it."
70 ),
71 }
72 }
73 let mut directions = HashMap::new();
74 for (index, state) in states.iter().enumerate() {
75 directions
76 .entry((state.book.base_address.clone(), state.book.quote_address.clone()))
77 .or_insert(index);
78 }
79 for (index, state) in states.iter().enumerate() {
80 directions
81 .entry((state.book.quote_address.clone(), state.book.base_address.clone()))
82 .or_insert(index);
83 }
84 let mut directions: Vec<_> = directions.into_iter().collect();
85 directions.sort();
86 Self { states: Arc::new(states), directions: Arc::new(directions), used: false }
87 }
88
89 fn pair_state(
91 &self,
92 token_in: &Bytes,
93 token_out: &Bytes,
94 ) -> Result<&NativeState, SimulationError> {
95 let index = self
96 .directions
97 .binary_search_by(|((a, b), _)| (a, b).cmp(&(token_in, token_out)))
98 .map_err(|_| {
99 SimulationError::InvalidInput(
100 format!("Invalid token addresses. Got in={token_in}, out={token_out}"),
101 None,
102 )
103 })?;
104 Ok(&self.states[self.directions[index].1])
105 }
106
107 fn quotable_pair_state(
109 &self,
110 token_in: &Bytes,
111 token_out: &Bytes,
112 ) -> Result<&NativeState, SimulationError> {
113 let state = self.pair_state(token_in, token_out)?;
114 if self.used {
115 return Err(SimulationError::RecoverableError(
116 "Native already quoted in this route".to_string(),
117 ));
118 }
119 Ok(state)
120 }
121
122 fn used_state(&self) -> Box<dyn ProtocolSim> {
123 Box::new(Self {
124 states: self.states.clone(),
125 directions: self.directions.clone(),
126 used: true,
127 })
128 }
129}
130
131#[typetag::serde]
132impl ProtocolSim for NativeAllPairsState {
133 fn fee(&self) -> f64 {
134 0.0
135 }
136
137 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
138 self.quotable_pair_state(&base.address, "e.address)?
139 .spot_price(base, quote)
140 }
141
142 fn get_amount_out(
143 &self,
144 amount_in: BigUint,
145 token_in: &Token,
146 token_out: &Token,
147 ) -> Result<GetAmountOutResult, SimulationError> {
148 let state = self.quotable_pair_state(&token_in.address, &token_out.address)?;
149 match state.get_amount_out(amount_in, token_in, token_out) {
150 Ok(mut res) => {
151 res.new_state = self.used_state();
152 Ok(res)
153 }
154 Err(SimulationError::InvalidInput(message, Some(mut res))) => {
155 res.new_state = self.used_state();
156 Err(SimulationError::InvalidInput(message, Some(res)))
157 }
158 Err(e) => Err(e),
159 }
160 }
161
162 fn get_limits(
163 &self,
164 sell_token: Bytes,
165 buy_token: Bytes,
166 ) -> Result<(BigUint, BigUint), SimulationError> {
167 self.quotable_pair_state(&sell_token, &buy_token)?
168 .get_limits(sell_token, buy_token)
169 }
170
171 fn delta_transition(
172 &mut self,
173 _delta: ProtocolStateDelta,
174 _tokens: &HashMap<Bytes, Token>,
175 _balances: &Balances,
176 ) -> Result<(), TransitionError> {
177 Err(TransitionError::DecodeError("Not implemented".into()))
178 }
179
180 fn clone_box(&self) -> Box<dyn ProtocolSim> {
181 Box::new(self.clone())
182 }
183
184 fn as_any(&self) -> &dyn Any {
185 self
186 }
187
188 fn as_any_mut(&mut self) -> &mut dyn Any {
189 self
190 }
191
192 fn eq(&self, other: &dyn ProtocolSim) -> bool {
193 let Some(other) = other
194 .as_any()
195 .downcast_ref::<NativeAllPairsState>()
196 else {
197 return false;
198 };
199 self.used == other.used &&
200 self.states.len() == other.states.len() &&
201 self.states
202 .iter()
203 .zip(other.states.iter())
204 .all(|(a, b)| a.book == b.book)
205 }
206
207 fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
208 Ok(self)
209 }
210}
211
212#[async_trait]
213impl IndicativelyPriced for NativeAllPairsState {
214 async fn request_signed_quote(
215 &self,
216 params: GetAmountOutParams,
217 ) -> Result<SignedQuote, SimulationError> {
218 self.pair_state(¶ms.token_in, ¶ms.token_out)?
219 .request_signed_quote(params)
220 .await
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use std::collections::HashSet;
227
228 use tokio::time::Duration;
229 use tycho_common::models::Chain;
230
231 use super::*;
232 use crate::rfq::{
233 models::ComponentLayout,
234 protocols::{
235 native::models::NativePriceLevel,
236 test_utils::{token, usdc, weth},
237 },
238 };
239
240 fn book() -> NativePriceData {
241 NativePriceData {
242 base_address: weth().address,
243 quote_address: usdc().address,
244 minimum_in_base: 100_000_000_000.0,
245 minimum_in_quote: 100.0,
246 minimum_out_base: 0.0,
247 minimum_out_quote: 0.0,
248 bids: vec![NativePriceLevel { quantity: 1.0, price: 2_000.0 }],
249 asks: vec![NativePriceLevel { quantity: 1.0, price: 2_000.0 }],
250 }
251 }
252
253 fn state_with(books: Vec<NativePriceData>) -> NativeAllPairsState {
254 let client = NativeClient::new(
255 Chain::Ethereum,
256 String::new(),
257 HashSet::new(),
258 0.0,
259 HashSet::new(),
260 Duration::from_secs(5),
261 Duration::from_secs(5),
262 )
263 .unwrap()
264 .with_component_layout(ComponentLayout::AllPairs);
265 NativeAllPairsState::new(
266 books,
267 HashMap::from([(weth().address, weth()), (usdc().address, usdc())]),
268 client,
269 )
270 }
271
272 fn state() -> NativeAllPairsState {
273 state_with(vec![book()])
274 }
275
276 #[test]
277 fn once_per_venue() {
278 let state = state();
279 let first = state
280 .get_amount_out(BigUint::from(500_000_000_000_000_000u64), &weth(), &usdc())
281 .unwrap();
282 let after_first = first
283 .new_state
284 .as_any()
285 .downcast_ref::<NativeAllPairsState>()
286 .unwrap();
287 assert!(after_first.used);
288 assert!(matches!(
289 after_first.get_amount_out(BigUint::from(1_000_000_000u64), &usdc(), &weth()),
290 Err(SimulationError::RecoverableError(message)) if message == "Native already quoted in this route"
291 ));
292 assert!(matches!(
293 after_first.spot_price(&weth(), &usdc()),
294 Err(SimulationError::RecoverableError(message)) if message == "Native already quoted in this route"
295 ));
296 assert!(matches!(
297 after_first.get_limits(weth().address, usdc().address),
298 Err(SimulationError::RecoverableError(message)) if message == "Native already quoted in this route"
299 ));
300 }
301
302 #[test]
303 fn book_quoting_the_direction_beats_the_inverted_one() {
304 let mut forward = book();
307 forward.minimum_out_base = 2_000_000_000_000_000_000.0;
308 let mut reverse = book();
309 reverse.base_address = usdc().address;
310 reverse.quote_address = weth().address;
311 reverse.minimum_in_base = 0.0;
312 reverse.bids = vec![NativePriceLevel { quantity: 2_000.0, price: 0.0005 }];
313 reverse.asks = vec![];
314 let state = state_with(vec![forward, reverse]);
315 let result = state
316 .get_amount_out(BigUint::from(2_000_000_000u64), &usdc(), &weth())
317 .unwrap();
318 assert_eq!(result.amount, BigUint::from(1_000_000_000_000_000_000u64));
319 }
320
321 #[test]
322 fn skips_book_its_state_rejects() {
323 let mut invalid = book();
324 invalid.minimum_in_base = f64::NAN;
325 let state = state_with(vec![invalid]);
326 let price = state.spot_price(&weth(), &usdc());
327 assert!(
328 matches!(price, Err(SimulationError::InvalidInput(msg, None)) if msg == format!("Invalid token addresses. Got in={}, out={}", weth().address, usdc().address))
329 );
330 }
331
332 #[test]
333 fn returns_partial_result_when_amount_exceeds_depth() {
334 let state = state();
335 let result =
336 state.get_amount_out(BigUint::from(2_000_000_000_000_000_000u64), &weth(), &usdc());
337 let Err(SimulationError::InvalidInput(_, Some(partial))) = result else {
338 panic!("Expected insufficient-liquidity result, got {result:?}");
339 };
340 assert_eq!(partial.amount, BigUint::from(2_000_000_000u64));
341 let new_state = partial
342 .new_state
343 .as_any()
344 .downcast_ref::<NativeAllPairsState>()
345 .unwrap();
346 assert!(new_state.used);
347 }
348
349 #[test]
350 fn rejects_invalid_pair() {
351 let other = token("0x1111111111111111111111111111111111111111", "OTHER", 18);
352 let state = state();
353 assert!(matches!(
354 state.get_amount_out(BigUint::from(1u64), &other, &usdc()),
355 Err(SimulationError::InvalidInput(_, None))
356 ));
357 assert!(matches!(
358 state.get_limits(other.address.clone(), usdc().address),
359 Err(SimulationError::InvalidInput(_, None))
360 ));
361 let mut empty = book();
363 empty.bids.clear();
364 empty.asks.clear();
365 let state = state_with(vec![empty]);
366 assert!(matches!(
367 state.spot_price(&other, &usdc()),
368 Err(SimulationError::InvalidInput(message, None))
369 if message.contains("Invalid token addresses")
370 ));
371 }
372
373 #[test]
374 fn reports_no_liquidity_for_empty_direction() {
375 let mut book = book();
376 book.bids.clear();
377 let state = state_with(vec![book]);
378 assert!(matches!(
379 state.get_amount_out(BigUint::from(500_000_000_000_000_000u64), &weth(), &usdc()),
380 Err(SimulationError::RecoverableError(_))
381 ));
382 assert!(matches!(
383 state.get_limits(weth().address, usdc().address),
384 Err(SimulationError::RecoverableError(_))
385 ));
386 }
387}