1use std::{any::Any, collections::HashMap, fmt};
2
3use async_trait::async_trait;
4use num_bigint::BigUint;
5use num_traits::{FromPrimitive, ToPrimitive, Zero};
6use serde::{Deserialize, Serialize};
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::metric::{
19 client::MetricClient,
20 models::{MetricBidAskResponse, MetricDepthBin, MetricMetadata},
21};
22
23#[derive(Clone, Serialize, Deserialize)]
24pub struct MetricState {
25 pub base_token: Token,
26 pub quote_token: Token,
27 pub metadata: MetricMetadata,
28 pub bid_ask: MetricBidAskResponse,
29 pub client: MetricClient,
30}
31
32impl fmt::Debug for MetricState {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 f.debug_struct("MetricState")
35 .field("base_token", &self.base_token)
36 .field("quote_token", &self.quote_token)
37 .field("pool", &self.metadata.pool_address)
38 .field("latest_block", &self.bid_ask.latest_block)
39 .finish_non_exhaustive()
40 }
41}
42
43impl MetricState {
44 pub fn new(
45 base_token: Token,
46 quote_token: Token,
47 metadata: MetricMetadata,
48 bid_ask: MetricBidAskResponse,
49 client: MetricClient,
50 ) -> Self {
51 Self { base_token, quote_token, metadata, bid_ask, client }
52 }
53
54 fn direction(
55 &self,
56 token_in: &Bytes,
57 token_out: &Bytes,
58 ) -> Result<MetricDirection, SimulationError> {
59 if token_in == &self.base_token.address && token_out == &self.quote_token.address {
60 Ok(MetricDirection::ZeroForOne)
61 } else if token_in == &self.quote_token.address && token_out == &self.base_token.address {
62 Ok(MetricDirection::OneForZero)
63 } else {
64 Err(SimulationError::InvalidInput(
65 format!(
66 "Invalid token addresses. Got in={token_in}, out={token_out}, expected {} / {}",
67 self.base_token.address, self.quote_token.address
68 ),
69 None,
70 ))
71 }
72 }
73
74 fn quote_with_depth(
75 &self,
76 direction: MetricDirection,
77 amount_in_human: f64,
78 token_out_decimals: u32,
79 max_output: &BigUint,
80 ) -> Result<Option<DepthQuote>, SimulationError> {
81 let (start_price, bins) = match direction {
82 MetricDirection::ZeroForOne => (self.bid_ask.bid_price()?, &self.bid_ask.depth.bids),
83 MetricDirection::OneForZero => (self.bid_ask.ask_price()?, &self.bid_ask.depth.asks),
84 };
85
86 let Some(depth_max_output) = depth_max_output(bins)? else {
89 return Ok(None);
90 };
91
92 let effective_max_output = depth_max_output.min(max_output.clone());
93 if effective_max_output.is_zero() {
94 return Ok(Some(DepthQuote {
95 amount_out_human: 0.0,
96 max_output: effective_max_output,
97 exhausted: amount_in_human > 0.0,
98 }));
99 }
100
101 let max_output_human =
102 raw_to_human(&effective_max_output, token_out_decimals, "depth max output")?;
103 let depth_fill = depth_output_for_input(
104 direction,
105 bins,
106 start_price,
107 amount_in_human,
108 token_out_decimals,
109 max_output_human,
110 )?;
111
112 Ok(Some(DepthQuote {
113 amount_out_human: depth_fill.output_human,
114 max_output: effective_max_output,
115 exhausted: depth_fill.exhausted,
116 }))
117 }
118}
119
120#[derive(Debug, Clone, Copy)]
121enum MetricDirection {
122 ZeroForOne,
123 OneForZero,
124}
125
126struct DepthQuote {
127 amount_out_human: f64,
128 max_output: BigUint,
129 exhausted: bool,
130}
131
132struct DepthFill {
133 output_human: f64,
134 exhausted: bool,
135}
136
137#[typetag::serde]
138impl ProtocolSim for MetricState {
139 fn fee(&self) -> f64 {
140 0.0
141 }
142
143 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
144 let bid = self.bid_ask.bid_price()?;
145 let ask = self.bid_ask.ask_price()?;
146 let mid = (bid + ask) / 2.0;
147 if base.address == self.base_token.address && quote.address == self.quote_token.address {
148 Ok(mid)
149 } else if base.address == self.quote_token.address &&
150 quote.address == self.base_token.address
151 {
152 Ok(1.0 / mid)
153 } else {
154 Err(SimulationError::InvalidInput(
155 format!(
156 "Invalid token addresses. Got base={}, quote={}, expected {} / {}",
157 base.address, quote.address, self.base_token.address, self.quote_token.address
158 ),
159 None,
160 ))
161 }
162 }
163
164 fn get_amount_out(
165 &self,
166 amount_in: BigUint,
167 token_in: &Token,
168 token_out: &Token,
169 ) -> Result<GetAmountOutResult, SimulationError> {
170 let direction = self.direction(&token_in.address, &token_out.address)?;
171 let amount_in_human = amount_in.to_f64().ok_or_else(|| {
172 SimulationError::RecoverableError("Can't convert amount in to f64".into())
173 })? / 10_f64.powi(token_in.decimals as i32);
174
175 let (flat_amount_out_human, max_output) = match direction {
176 MetricDirection::ZeroForOne => {
177 let price = self.bid_ask.bid_price()?;
178 (amount_in_human * price, self.bid_ask.total_token1_available()?)
179 }
180 MetricDirection::OneForZero => {
181 let price = self.bid_ask.ask_price()?;
182 (amount_in_human / price, self.bid_ask.total_token0_available()?)
183 }
184 };
185 let depth_quote =
188 self.quote_with_depth(direction, amount_in_human, token_out.decimals, &max_output)?;
189 let (amount_out_human, effective_max_output, exhausted) = match depth_quote {
190 Some(quote) => (quote.amount_out_human, quote.max_output, quote.exhausted),
191 None => (flat_amount_out_human, max_output.clone(), false),
192 };
193
194 let amount_out =
195 BigUint::from_f64(amount_out_human * 10_f64.powi(token_out.decimals as i32))
196 .ok_or_else(|| {
197 SimulationError::RecoverableError("Can't convert amount out to BigUint".into())
198 })?;
199 let capped_amount = if exhausted {
204 effective_max_output.clone()
205 } else {
206 amount_out
207 .clone()
208 .min(effective_max_output.clone())
209 };
210 let res = GetAmountOutResult {
211 amount: capped_amount.clone(),
212 gas: BigUint::from(170_000u64),
213 new_state: self.clone_box(),
214 };
215
216 if amount_out > effective_max_output {
217 return Err(SimulationError::InvalidInput(
218 format!(
219 "Metric pool has not enough liquidity. Requested output {amount_out} exceeds \
220 available {effective_max_output}"
221 ),
222 Some(res),
223 ));
224 }
225 if exhausted {
226 return Err(SimulationError::InvalidInput(
227 format!(
228 "Metric pool depth exhausted. Input {amount_in} cannot be fully filled; \
229 tradable depth caps output at {effective_max_output}"
230 ),
231 Some(res),
232 ));
233 }
234
235 Ok(res)
236 }
237
238 fn get_limits(
239 &self,
240 sell_token: Bytes,
241 buy_token: Bytes,
242 ) -> Result<(BigUint, BigUint), SimulationError> {
243 let direction = self.direction(&sell_token, &buy_token)?;
244 match direction {
245 MetricDirection::ZeroForOne => {
246 let price = self.bid_ask.bid_price()?;
247 let buy_limit =
251 cap_to_depth(self.bid_ask.total_token1_available()?, &self.bid_ask.depth.bids)?;
252 let buy_limit_human = buy_limit.to_f64().ok_or_else(|| {
253 SimulationError::RecoverableError("Can't convert buy limit to f64".into())
254 })? / 10_f64.powi(self.quote_token.decimals as i32);
255 let sell_limit = buy_limit_human / price;
256 let sell_limit =
257 BigUint::from_f64(sell_limit * 10_f64.powi(self.base_token.decimals as i32))
258 .ok_or_else(|| {
259 SimulationError::RecoverableError(
260 "Can't convert sell limit to BigUint".into(),
261 )
262 })?;
263 Ok((sell_limit, buy_limit))
264 }
265 MetricDirection::OneForZero => {
266 let price = self.bid_ask.ask_price()?;
267 let buy_limit =
271 cap_to_depth(self.bid_ask.total_token0_available()?, &self.bid_ask.depth.asks)?;
272 let buy_limit_human = buy_limit.to_f64().ok_or_else(|| {
273 SimulationError::RecoverableError("Can't convert buy limit to f64".into())
274 })? / 10_f64.powi(self.base_token.decimals as i32);
275 let sell_limit = buy_limit_human * price;
276 let sell_limit =
277 BigUint::from_f64(sell_limit * 10_f64.powi(self.quote_token.decimals as i32))
278 .ok_or_else(|| {
279 SimulationError::RecoverableError(
280 "Can't convert sell limit to BigUint".into(),
281 )
282 })?;
283 Ok((sell_limit, buy_limit))
284 }
285 }
286 }
287
288 fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
289 Ok(self)
290 }
291
292 fn delta_transition(
293 &mut self,
294 _delta: ProtocolStateDelta,
295 _tokens: &HashMap<Bytes, Token>,
296 _balances: &Balances,
297 ) -> Result<(), TransitionError> {
298 Err(TransitionError::DecodeError(
300 "Metric RFQ state is snapshot-based and does not support deltas".into(),
301 ))
302 }
303
304 fn clone_box(&self) -> Box<dyn ProtocolSim> {
305 Box::new(self.clone())
306 }
307
308 fn as_any(&self) -> &dyn Any {
309 self
310 }
311
312 fn as_any_mut(&mut self) -> &mut dyn Any {
313 self
314 }
315
316 fn eq(&self, other: &dyn ProtocolSim) -> bool {
317 if let Some(other_state) = other
318 .as_any()
319 .downcast_ref::<MetricState>()
320 {
321 self.base_token == other_state.base_token &&
322 self.quote_token == other_state.quote_token &&
323 self.metadata == other_state.metadata &&
324 self.bid_ask == other_state.bid_ask
325 } else {
326 false
327 }
328 }
329}
330
331fn depth_max_output(bins: &[MetricDepthBin]) -> Result<Option<BigUint>, SimulationError> {
332 bins.last()
333 .map(|bin| {
334 bin.cumulative_volume()
335 .map_err(SimulationError::from)
336 })
337 .transpose()
338}
339
340fn cap_to_depth(aggregate: BigUint, bins: &[MetricDepthBin]) -> Result<BigUint, SimulationError> {
346 match depth_max_output(bins)? {
347 Some(depth) => Ok(depth.min(aggregate)),
348 None => Ok(aggregate),
349 }
350}
351
352fn depth_output_for_input(
353 direction: MetricDirection,
354 bins: &[MetricDepthBin],
355 start_price: f64,
356 input_human: f64,
357 output_decimals: u32,
358 max_output_human: f64,
359) -> Result<DepthFill, SimulationError> {
360 if input_human == 0.0 || max_output_human == 0.0 {
361 return Ok(DepthFill { output_human: 0.0, exhausted: input_human > 0.0 });
362 }
363
364 let mut current_price = start_price;
365 let mut previous_volume = 0.0;
366 let mut remaining_input = input_human;
367 let mut output_human = 0.0;
368
369 for bin in bins {
370 let bin_price = bin.price()?;
371 let cumulative =
374 raw_to_human(&bin.cumulative_volume()?, output_decimals, "depth cumulative volume")?;
375 let volume_in_bin = cumulative - previous_volume;
376 previous_volume = cumulative;
377
378 if volume_in_bin <= 0.0 {
379 current_price = bin_price;
380 continue;
381 }
382
383 let output_capacity = max_output_human - output_human;
384 if output_capacity <= 0.0 {
385 break;
386 }
387 let fillable_volume = volume_in_bin.min(output_capacity);
388 let segment_exit_price =
390 current_price + (bin_price - current_price) * fillable_volume / volume_in_bin;
391 let full_segment_input = depth_segment_input_required(
394 direction,
395 fillable_volume,
396 current_price,
397 segment_exit_price,
398 )?;
399
400 if remaining_input >= full_segment_input {
401 output_human += fillable_volume;
402 remaining_input -= full_segment_input;
403 current_price = segment_exit_price;
404 continue;
405 }
406
407 output_human += depth_segment_output_for_input(
411 direction,
412 remaining_input,
413 fillable_volume,
414 current_price,
415 segment_exit_price,
416 )?;
417 remaining_input = 0.0;
418 break;
419 }
420
421 if remaining_input > 1e-12 && output_human < max_output_human - 1e-12 {
422 return Err(SimulationError::RecoverableError(
423 "Metric depth has not enough cumulative volume".into(),
424 ));
425 }
426
427 Ok(DepthFill {
428 output_human: output_human.min(max_output_human),
429 exhausted: remaining_input > 1e-12,
430 })
431}
432
433fn depth_segment_input_required(
434 direction: MetricDirection,
435 output_human: f64,
436 entry_price: f64,
437 exit_price: f64,
438) -> Result<f64, SimulationError> {
439 let average_price = depth_segment_average_price(entry_price, exit_price)?;
440 match direction {
441 MetricDirection::ZeroForOne => Ok(output_human / average_price),
442 MetricDirection::OneForZero => Ok(output_human * average_price),
443 }
444}
445
446fn depth_segment_output_for_input(
447 direction: MetricDirection,
448 input_human: f64,
449 segment_output_human: f64,
450 entry_price: f64,
451 exit_price: f64,
452) -> Result<f64, SimulationError> {
453 if segment_output_human <= 0.0 || entry_price <= 0.0 {
454 return Err(SimulationError::RecoverableError(
455 "Metric depth has invalid price curve".into(),
456 ));
457 }
458
459 let average_slope = (exit_price - entry_price) / (2.0 * segment_output_human);
480 let output = match direction {
481 MetricDirection::ZeroForOne => {
482 let denominator = 1.0 - input_human * average_slope;
492 if denominator <= 0.0 {
493 return Err(SimulationError::RecoverableError(
494 "Metric depth has invalid price curve".into(),
495 ));
496 }
497 input_human * entry_price / denominator
498 }
499 MetricDirection::OneForZero => {
500 if average_slope.abs() < 1e-18 {
521 input_human / entry_price
522 } else {
523 let discriminant =
524 entry_price.mul_add(entry_price, 4.0 * average_slope * input_human);
525 if discriminant < 0.0 {
526 return Err(SimulationError::RecoverableError(
527 "Metric depth has invalid price curve".into(),
528 ));
529 }
530 let root_a = (-entry_price + discriminant.sqrt()) / (2.0 * average_slope);
531 let root_b = (-entry_price - discriminant.sqrt()) / (2.0 * average_slope);
532 [root_a, root_b]
533 .into_iter()
534 .find(|root| {
535 root.is_finite() && *root >= -1e-12 && *root <= segment_output_human + 1e-12
536 })
537 .ok_or_else(|| {
538 SimulationError::RecoverableError(
539 "Metric depth has invalid price curve".into(),
540 )
541 })?
542 }
543 }
544 };
545
546 Ok(output.clamp(0.0, segment_output_human))
547}
548
549fn depth_segment_average_price(entry_price: f64, exit_price: f64) -> Result<f64, SimulationError> {
550 let average_price = (entry_price + exit_price) / 2.0;
551 if average_price <= 0.0 {
552 return Err(SimulationError::RecoverableError(
553 "Metric depth has non-positive average price".into(),
554 ));
555 }
556 Ok(average_price)
557}
558
559fn raw_to_human(amount: &BigUint, decimals: u32, field: &str) -> Result<f64, SimulationError> {
560 let amount = amount.to_f64().ok_or_else(|| {
561 SimulationError::RecoverableError(format!("Can't convert {field} to f64"))
562 })?;
563 Ok(amount / 10_f64.powi(decimals as i32))
564}
565
566#[async_trait]
567impl IndicativelyPriced for MetricState {
568 async fn request_signed_quote(
569 &self,
570 params: GetAmountOutParams,
571 ) -> Result<SignedQuote, SimulationError> {
572 let direction = self.direction(¶ms.token_in, ¶ms.token_out)?;
573 let (token_in, token_out) = match direction {
574 MetricDirection::ZeroForOne => (&self.base_token, &self.quote_token),
575 MetricDirection::OneForZero => (&self.quote_token, &self.base_token),
576 };
577 let amount_out = self
578 .get_amount_out(params.amount_in.clone(), token_in, token_out)?
579 .amount;
580
581 Ok(self
584 .client
585 .request_oracle_update_for_pool(&self.metadata, ¶ms, amount_out)
586 .await?)
587 }
588}
589
590#[cfg(test)]
591mod tests {
592 use std::{collections::HashSet, str::FromStr};
593
594 use tokio::time::Duration;
595 use tycho_common::models::Chain;
596
597 use super::*;
598 use crate::rfq::protocols::metric::{client::MetricClient, models::MetricDepth};
599
600 fn weth() -> Token {
601 Token::new(
602 &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
603 "WETH",
604 18,
605 0,
606 &[Some(2300)],
607 Chain::Ethereum,
608 100,
609 )
610 }
611
612 fn usdc() -> Token {
613 Token::new(
614 &Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
615 "USDC",
616 6,
617 0,
618 &[Some(1)],
619 Chain::Ethereum,
620 100,
621 )
622 }
623
624 fn state() -> MetricState {
625 let weth = weth();
626 let usdc = usdc();
627 let metadata = MetricMetadata {
628 pool_address: Bytes::from_str("0xbF48bCf474d57fF82A3215319229e0DE1476A557").unwrap(),
629 token0: weth.address.clone(),
630 token1: usdc.address.clone(),
631 };
632 let bid_ask = MetricBidAskResponse {
633 bid_adj: "55340232221128654848000".to_string(),
635 ask_adj: "55524699661865750400000".to_string(),
637 quote_available: true,
638 total_token0_available: "10000000000000000000".to_string(),
639 total_token1_available: "30000000000".to_string(),
640 latest_block: 100,
641 depth: MetricDepth::default(),
642 };
643 let client = MetricClient::new(
644 Chain::Ethereum,
645 HashSet::new(),
646 0.0,
647 HashSet::new(),
648 "http://localhost:8080".to_string(),
649 None,
650 Duration::from_secs(1),
651 Duration::from_secs(1),
652 )
653 .unwrap();
654 MetricState::new(weth, usdc, metadata, bid_ask, client)
655 }
656
657 #[test]
658 fn test_get_amount_out_zero_for_one() {
659 let state = state();
660 let result = state
661 .get_amount_out(
662 BigUint::from(1_000_000_000_000_000_000u128),
663 &state.base_token,
664 &state.quote_token,
665 )
666 .unwrap();
667
668 assert_eq!(result.amount, BigUint::from(3_000_000_000u64));
669 }
670
671 #[test]
672 fn test_get_amount_out_one_for_zero() {
673 let state = state();
674 let result = state
675 .get_amount_out(BigUint::from(3_010_000_000u64), &state.quote_token, &state.base_token)
676 .unwrap();
677
678 assert_eq!(result.amount, BigUint::from(1_000_000_000_000_000_000u128));
679 }
680
681 #[test]
682 fn test_get_amount_out_caps_to_available_liquidity() {
683 let mut state = state();
684 state.bid_ask.total_token1_available = "1500000000".to_string();
685 let err = state
686 .get_amount_out(
687 BigUint::from(1_000_000_000_000_000_000u128),
688 &state.base_token,
689 &state.quote_token,
690 )
691 .unwrap_err();
692
693 assert!(matches!(err, SimulationError::InvalidInput(_, Some(_))));
694 }
695
696 #[test]
697 fn test_get_amount_out_depth_exhausted_reports_depth_message() {
698 let mut state = state();
699 state.bid_ask.depth.bids = vec![MetricDepthBin {
700 bin_idx: 0,
701 price: "53495557813757699686400".to_string(),
703 cumulative_volume: "3000000000".to_string(),
705 }];
706
707 let err = state
708 .get_amount_out(
709 BigUint::from(2_000_000_000_000_000_000u128),
711 &state.base_token,
712 &state.quote_token,
713 )
714 .unwrap_err();
715
716 match err {
717 SimulationError::InvalidInput(msg, Some(_)) => {
718 assert!(msg.contains("depth exhausted"), "unexpected message: {msg}");
719 }
720 other => panic!("expected InvalidInput, got {other:?}"),
721 }
722 }
723
724 #[test]
725 fn test_get_amount_out_exhausted_returns_exact_cap() {
726 let mut state = state();
727 let cap = "6575581573690662958";
730 state.bid_ask.depth.asks = vec![MetricDepthBin {
731 bin_idx: 0,
732 price: "57184906628499610009600".to_string(),
734 cumulative_volume: cap.to_string(),
735 }];
736
737 let err = state
738 .get_amount_out(
739 BigUint::from(30_000_000_000u64),
741 &state.quote_token,
742 &state.base_token,
743 )
744 .unwrap_err();
745
746 match err {
747 SimulationError::InvalidInput(msg, Some(res)) => {
748 assert!(msg.contains("depth exhausted"), "unexpected message: {msg}");
749 assert_eq!(res.amount, BigUint::from_str(cap).unwrap());
751 }
752 other => panic!("expected InvalidInput, got {other:?}"),
753 }
754 }
755
756 #[test]
757 fn test_get_limits_caps_to_depth() {
758 let mut state = state();
759 state.bid_ask.depth.bids = vec![MetricDepthBin {
760 bin_idx: 0,
761 price: "53495557813757699686400".to_string(),
763 cumulative_volume: "1500000000".to_string(),
765 }];
766
767 let (sell_limit, buy_limit) = state
768 .get_limits(state.base_token.address.clone(), state.quote_token.address.clone())
769 .unwrap();
770
771 assert_eq!(buy_limit, BigUint::from(1_500_000_000u64));
773 assert_eq!(sell_limit, BigUint::from(500_000_000_000_000_000u128));
775 }
776
777 #[test]
778 fn test_get_limits_uses_aggregate_without_depth() {
779 let state = state();
780
781 let (_, buy_limit) = state
782 .get_limits(state.base_token.address.clone(), state.quote_token.address.clone())
783 .unwrap();
784
785 assert_eq!(buy_limit, BigUint::from(30_000_000_000u64));
787 }
788
789 #[test]
790 fn test_get_amount_out_walks_bid_depth() {
791 let mut state = state();
792 state.bid_ask.depth.bids = vec![MetricDepthBin {
793 bin_idx: 0,
794 price: "53495557813757699686400".to_string(),
796 cumulative_volume: "3000000000".to_string(),
797 }];
798
799 let result = state
800 .get_amount_out(
801 BigUint::from(1_000_000_000_000_000_000u128),
802 &state.base_token,
803 &state.quote_token,
804 )
805 .unwrap();
806
807 assert!(result.amount < BigUint::from(3_000_000_000u64));
808 assert!(result.amount > BigUint::from(2_950_000_000u64));
809 }
810
811 #[test]
812 fn test_get_amount_out_walks_ask_depth() {
813 let mut state = state();
814 state.bid_ask.depth.asks = vec![MetricDepthBin {
815 bin_idx: 0,
816 price: "57184906628499610009600".to_string(),
818 cumulative_volume: "1000000000000000000".to_string(),
819 }];
820
821 let result = state
822 .get_amount_out(BigUint::from(3_000_000_000u64), &state.quote_token, &state.base_token)
823 .unwrap();
824
825 assert!(result.amount < BigUint::from(1_000_000_000_000_000_000u128));
826 assert!(result.amount > BigUint::from(980_000_000_000_000_000u128));
827 }
828
829 #[test]
830 fn test_depth_output_for_input_partially_fills_bid_bin() {
831 let bins = vec![MetricDepthBin {
832 bin_idx: 0,
833 price: "53495557813757699686400".to_string(),
835 cumulative_volume: "3000000000".to_string(),
836 }];
837
838 let fill =
839 depth_output_for_input(MetricDirection::ZeroForOne, &bins, 3000.0, 1.0, 6, 3000.0)
840 .unwrap();
841
842 assert!((fill.output_human - 2950.8196721311474).abs() < 1e-9);
843 assert!(!fill.exhausted);
844 }
845
846 #[test]
847 fn test_depth_output_for_input_partially_fills_ask_bin() {
848 let bins = vec![MetricDepthBin {
849 bin_idx: 0,
850 price: "57184906628499610009600".to_string(),
852 cumulative_volume: "1000000000000000000".to_string(),
853 }];
854
855 let fill =
856 depth_output_for_input(MetricDirection::OneForZero, &bins, 3010.0, 3000.0, 18, 1.0)
857 .unwrap();
858
859 assert!((fill.output_human - 0.9822534928279816).abs() < 1e-12);
860 assert!(!fill.exhausted);
861 }
862
863 #[test]
864 fn test_depth_output_for_input_exhausts_available_depth() {
865 let bins = vec![MetricDepthBin {
866 bin_idx: 0,
867 price: "53495557813757699686400".to_string(),
869 cumulative_volume: "3000000000".to_string(),
870 }];
871
872 let fill =
873 depth_output_for_input(MetricDirection::ZeroForOne, &bins, 3000.0, 2.0, 6, 3000.0)
874 .unwrap();
875
876 assert_eq!(fill.output_human, 3000.0);
877 assert!(fill.exhausted);
878 }
879
880 #[tokio::test]
881 #[ignore = "hits Metric's public API"]
882 async fn test_live_metric_api_state_get_amount_out_and_oracle_update() {
883 let weth = weth();
884 let usdc = usdc();
885 let base_url = std::env::var("METRIC_API_URL")
886 .unwrap_or_else(|_| "http://54.199.103.16:8080".to_string())
887 .trim_end_matches('/')
888 .to_string();
889 let client = MetricClient::new(
890 Chain::Ethereum,
891 HashSet::from([weth.address.clone(), usdc.address.clone()]),
892 0.0,
893 HashSet::new(),
894 base_url.clone(),
895 None,
896 Duration::from_secs(1),
897 Duration::from_secs(5),
898 )
899 .unwrap();
900
901 let http_client = reqwest::Client::new();
902 let metadata: Vec<MetricMetadata> = http_client
903 .get(format!("{base_url}/ethereum/metadata"))
904 .header("accept", "application/json")
905 .send()
906 .await
907 .unwrap()
908 .json()
909 .await
910 .unwrap();
911
912 let mut selected = None;
913 for pool in metadata
914 .into_iter()
915 .filter(|pool| pool.token0 == weth.address && pool.token1 == usdc.address)
916 {
917 let bid_ask: MetricBidAskResponse = http_client
918 .get(format!("{base_url}/ethereum/{}/bid_ask", pool.pool_address))
919 .header("accept", "application/json")
920 .send()
921 .await
922 .unwrap()
923 .json()
924 .await
925 .unwrap();
926 let has_enough_quote_liquidity = bid_ask
927 .total_token1_available()
928 .map(|available| available > BigUint::from(10u8))
929 .unwrap_or(false);
930 if bid_ask.quote_available && has_enough_quote_liquidity {
931 selected = Some((pool, bid_ask));
932 break;
933 }
934 }
935
936 let Some((metadata, bid_ask)) = selected else {
937 eprintln!("Metric live API returned no liquid Ethereum WETH/USDC pool; skipping");
938 return;
939 };
940
941 let state = MetricState::new(weth, usdc, metadata, bid_ask, client);
942 assert!(state.bid_ask.quote_available);
943
944 let amount_in = BigUint::from(1_000_000_000u64);
945 let indicative_quote = state
946 .get_amount_out(amount_in.clone(), &state.base_token, &state.quote_token)
947 .unwrap();
948 let trader = Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap();
949 let signed_quote = state
950 .request_signed_quote(GetAmountOutParams {
951 amount_in,
952 token_in: state.base_token.address.clone(),
953 token_out: state.quote_token.address.clone(),
954 sender: trader.clone(),
955 receiver: trader,
956 })
957 .await
958 .unwrap();
959
960 assert!(indicative_quote.amount > BigUint::from(0u8));
961 assert!(signed_quote.amount_out > BigUint::from(0u8));
962 assert_eq!(signed_quote.base_token, state.base_token.address);
963 assert_eq!(signed_quote.quote_token, state.quote_token.address);
964 assert!(!signed_quote.quote_attributes["oracle_update_0_args"].is_empty());
965 }
966}