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
23const METRIC_SWAP_GAS: u64 = 170_000;
25
26#[derive(Clone, Serialize, Deserialize)]
27pub struct MetricState {
28 pub base_token: Token,
29 pub quote_token: Token,
30 pub metadata: MetricMetadata,
31 pub bid_ask: MetricBidAskResponse,
32 pub client: MetricClient,
33}
34
35impl fmt::Debug for MetricState {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 f.debug_struct("MetricState")
38 .field("base_token", &self.base_token)
39 .field("quote_token", &self.quote_token)
40 .field("pool", &self.metadata.pool_address)
41 .field("server_ts", &self.bid_ask.server_ts)
42 .finish_non_exhaustive()
43 }
44}
45
46impl MetricState {
47 pub fn new(
48 base_token: Token,
49 quote_token: Token,
50 metadata: MetricMetadata,
51 bid_ask: MetricBidAskResponse,
52 client: MetricClient,
53 ) -> Self {
54 Self { base_token, quote_token, metadata, bid_ask, client }
55 }
56
57 fn direction(
58 &self,
59 token_in: &Bytes,
60 token_out: &Bytes,
61 ) -> Result<MetricDirection, SimulationError> {
62 if token_in == &self.base_token.address && token_out == &self.quote_token.address {
63 Ok(MetricDirection::ZeroForOne)
64 } else if token_in == &self.quote_token.address && token_out == &self.base_token.address {
65 Ok(MetricDirection::OneForZero)
66 } else {
67 Err(SimulationError::InvalidInput(
68 format!(
69 "Invalid token addresses. Got in={token_in}, out={token_out}, expected {} / {}",
70 self.base_token.address, self.quote_token.address
71 ),
72 None,
73 ))
74 }
75 }
76
77 fn quote_with_depth(
78 &self,
79 direction: MetricDirection,
80 amount_in: &BigUint,
81 max_output: &BigUint,
82 ) -> Result<Option<DepthQuote>, SimulationError> {
83 let bins = match direction {
84 MetricDirection::ZeroForOne => &self.bid_ask.depth.bids,
85 MetricDirection::OneForZero => &self.bid_ask.depth.asks,
86 };
87
88 let Some(depth_max_output) = depth_max_output(bins) else {
92 return Ok(None);
93 };
94
95 let effective_max_output = depth_max_output.min(max_output.clone());
96 let depth_fill = depth_output_for_input(bins, amount_in, &effective_max_output)?;
97
98 Ok(Some(DepthQuote {
99 amount_out: depth_fill.output,
100 max_output: effective_max_output,
101 exhausted: depth_fill.exhausted,
102 }))
103 }
104}
105
106#[derive(Debug, Clone, Copy)]
107enum MetricDirection {
108 ZeroForOne,
109 OneForZero,
110}
111
112struct DepthQuote {
113 amount_out: BigUint,
114 max_output: BigUint,
115 exhausted: bool,
116}
117
118#[derive(Debug)]
119struct DepthFill {
120 output: BigUint,
121 exhausted: bool,
122}
123
124#[typetag::serde]
125impl ProtocolSim for MetricState {
126 fn fee(&self) -> f64 {
127 0.0
128 }
129
130 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
131 let bid = self.bid_ask.bid_price()?;
132 let ask = self.bid_ask.ask_price()?;
133 let mid = (bid + ask) / 2.0;
134 if base.address == self.base_token.address && quote.address == self.quote_token.address {
135 Ok(mid)
136 } else if base.address == self.quote_token.address &&
137 quote.address == self.base_token.address
138 {
139 Ok(1.0 / mid)
140 } else {
141 Err(SimulationError::InvalidInput(
142 format!(
143 "Invalid token addresses. Got base={}, quote={}, expected {} / {}",
144 base.address, quote.address, self.base_token.address, self.quote_token.address
145 ),
146 None,
147 ))
148 }
149 }
150
151 fn get_amount_out(
152 &self,
153 amount_in: BigUint,
154 token_in: &Token,
155 token_out: &Token,
156 ) -> Result<GetAmountOutResult, SimulationError> {
157 let direction = self.direction(&token_in.address, &token_out.address)?;
158 let max_output = match direction {
159 MetricDirection::ZeroForOne => self.bid_ask.total_token1_available()?,
160 MetricDirection::OneForZero => self.bid_ask.total_token0_available()?,
161 };
162
163 if let Some(quote) = self.quote_with_depth(direction, &amount_in, &max_output)? {
168 let res = GetAmountOutResult {
169 amount: quote.amount_out,
170 gas: BigUint::from(METRIC_SWAP_GAS),
171 new_state: self.clone_box(),
172 };
173 if quote.exhausted {
174 return Err(SimulationError::InvalidInput(
175 format!(
176 "Metric pool depth exhausted. Input {amount_in} cannot be fully filled; \
177 tradable depth caps output at {}",
178 quote.max_output
179 ),
180 Some(res),
181 ));
182 }
183 return Ok(res);
184 }
185
186 let amount_in_human = amount_in.to_f64().ok_or_else(|| {
188 SimulationError::RecoverableError("Can't convert amount in to f64".into())
189 })? / 10_f64.powi(token_in.decimals as i32);
190 let flat_amount_out_human = match direction {
191 MetricDirection::ZeroForOne => amount_in_human * self.bid_ask.bid_price()?,
192 MetricDirection::OneForZero => amount_in_human / self.bid_ask.ask_price()?,
193 };
194 let amount_out =
195 BigUint::from_f64(flat_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 res = GetAmountOutResult {
200 amount: amount_out
201 .clone()
202 .min(max_output.clone()),
203 gas: BigUint::from(METRIC_SWAP_GAS),
204 new_state: self.clone_box(),
205 };
206 if amount_out > max_output {
207 return Err(SimulationError::InvalidInput(
208 format!(
209 "Metric pool has not enough liquidity. Requested output {amount_out} exceeds \
210 available {max_output}"
211 ),
212 Some(res),
213 ));
214 }
215 Ok(res)
216 }
217
218 fn get_limits(
219 &self,
220 sell_token: Bytes,
221 buy_token: Bytes,
222 ) -> Result<(BigUint, BigUint), SimulationError> {
223 let direction = self.direction(&sell_token, &buy_token)?;
224 let (sell_per_buy, aggregate, bins, sell_decimals, buy_decimals) = match direction {
227 MetricDirection::ZeroForOne => (
228 1.0 / self.bid_ask.bid_price()?,
229 self.bid_ask.total_token1_available()?,
230 &self.bid_ask.depth.bids,
231 self.base_token.decimals,
232 self.quote_token.decimals,
233 ),
234 MetricDirection::OneForZero => (
235 self.bid_ask.ask_price()?,
236 self.bid_ask.total_token0_available()?,
237 &self.bid_ask.depth.asks,
238 self.quote_token.decimals,
239 self.base_token.decimals,
240 ),
241 };
242
243 if let Some(last_bin) = bins.last() {
247 if last_bin.cumulative_volume <= aggregate {
248 return Ok((
249 last_bin.cumulative_input_volume.clone(),
250 last_bin.cumulative_volume.clone(),
251 ));
252 }
253 }
254
255 let buy_limit = aggregate;
260 let buy_limit_human = buy_limit.to_f64().ok_or_else(|| {
261 SimulationError::RecoverableError("Can't convert buy limit to f64".into())
262 })? / 10_f64.powi(buy_decimals as i32);
263 let sell_limit =
264 BigUint::from_f64(buy_limit_human * sell_per_buy * 10_f64.powi(sell_decimals as i32))
265 .ok_or_else(|| {
266 SimulationError::RecoverableError("Can't convert sell limit to BigUint".into())
267 })?;
268 Ok((sell_limit, buy_limit))
269 }
270
271 fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
272 Ok(self)
273 }
274
275 fn delta_transition(
276 &mut self,
277 _delta: ProtocolStateDelta,
278 _tokens: &HashMap<Bytes, Token>,
279 _balances: &Balances,
280 ) -> Result<(), TransitionError> {
281 Err(TransitionError::DecodeError(
283 "Metric RFQ state is snapshot-based and does not support deltas".into(),
284 ))
285 }
286
287 fn clone_box(&self) -> Box<dyn ProtocolSim> {
288 Box::new(self.clone())
289 }
290
291 fn as_any(&self) -> &dyn Any {
292 self
293 }
294
295 fn as_any_mut(&mut self) -> &mut dyn Any {
296 self
297 }
298
299 fn eq(&self, other: &dyn ProtocolSim) -> bool {
300 if let Some(other_state) = other
301 .as_any()
302 .downcast_ref::<MetricState>()
303 {
304 self.base_token == other_state.base_token &&
305 self.quote_token == other_state.quote_token &&
306 self.metadata == other_state.metadata &&
307 self.bid_ask == other_state.bid_ask
308 } else {
309 false
310 }
311 }
312}
313
314fn depth_max_output(bins: &[MetricDepthBin]) -> Option<BigUint> {
315 bins.last()
316 .map(|bin| bin.cumulative_volume.clone())
317}
318
319fn depth_output_for_input(
328 bins: &[MetricDepthBin],
329 amount_in: &BigUint,
330 max_output: &BigUint,
331) -> Result<DepthFill, SimulationError> {
332 if amount_in.is_zero() || max_output.is_zero() {
333 return Ok(DepthFill { output: BigUint::ZERO, exhausted: !amount_in.is_zero() });
334 }
335
336 let mut previous_output = BigUint::ZERO;
337 let mut previous_input = BigUint::ZERO;
338 let mut remaining_input = amount_in.clone();
339 let mut output = BigUint::ZERO;
340
341 for bin in bins {
342 let cumulative_output = &bin.cumulative_volume;
346 let cumulative_input = &bin.cumulative_input_volume;
347 if cumulative_output < &previous_output || cumulative_input < &previous_input {
348 return Err(SimulationError::RecoverableError(
349 "Metric depth cumulative volumes are not monotonic".into(),
350 ));
351 }
352 let volume_in_bin = cumulative_output - &previous_output;
353 let input_in_bin = cumulative_input - &previous_input;
354 previous_output = cumulative_output.clone();
355 previous_input = cumulative_input.clone();
356
357 if volume_in_bin.is_zero() && input_in_bin.is_zero() {
359 continue;
360 }
361 if volume_in_bin.is_zero() || input_in_bin.is_zero() {
364 return Err(SimulationError::RecoverableError(
365 "Metric depth bin has inconsistent volume and input".into(),
366 ));
367 }
368
369 let output_capacity = max_output - &output;
370 if output_capacity.is_zero() {
371 break;
372 }
373
374 let (fillable_volume, fillable_input) = if volume_in_bin <= output_capacity {
377 (volume_in_bin, input_in_bin)
378 } else {
379 let fillable_input = (&input_in_bin * &output_capacity + &volume_in_bin -
380 BigUint::from(1u8)) /
381 &volume_in_bin;
382 (output_capacity, fillable_input)
383 };
384
385 if remaining_input >= fillable_input {
386 output += &fillable_volume;
387 remaining_input -= &fillable_input;
388 continue;
389 }
390
391 output += &fillable_volume * &remaining_input / &fillable_input;
393 remaining_input = BigUint::ZERO;
394 break;
395 }
396
397 Ok(DepthFill { output, exhausted: !remaining_input.is_zero() })
398}
399
400#[async_trait]
401impl IndicativelyPriced for MetricState {
402 async fn request_signed_quote(
403 &self,
404 params: GetAmountOutParams,
405 ) -> Result<SignedQuote, SimulationError> {
406 let direction = self.direction(¶ms.token_in, ¶ms.token_out)?;
407 let (token_in, token_out) = match direction {
408 MetricDirection::ZeroForOne => (&self.base_token, &self.quote_token),
409 MetricDirection::OneForZero => (&self.quote_token, &self.base_token),
410 };
411 let amount_out = self
412 .get_amount_out(params.amount_in.clone(), token_in, token_out)?
413 .amount;
414
415 Ok(SignedQuote {
418 base_token: params.token_in.clone(),
419 quote_token: params.token_out.clone(),
420 amount_in: params.amount_in.clone(),
421 amount_out,
422 quote_attributes: HashMap::new(),
423 })
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use std::{collections::HashSet, str::FromStr};
430
431 use tokio::time::Duration;
432 use tycho_common::models::Chain;
433
434 use super::*;
435 use crate::rfq::protocols::metric::{client::MetricClient, models::MetricDepth};
436
437 fn big(value: &str) -> BigUint {
438 value.parse().unwrap()
439 }
440
441 fn weth() -> Token {
442 Token::new(
443 &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
444 "WETH",
445 18,
446 0,
447 &[Some(2300)],
448 Chain::Ethereum,
449 100,
450 )
451 }
452
453 fn usdc() -> Token {
454 Token::new(
455 &Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
456 "USDC",
457 6,
458 0,
459 &[Some(1)],
460 Chain::Ethereum,
461 100,
462 )
463 }
464
465 fn base_weth() -> Token {
466 Token::new(
467 &Bytes::from_str("0x4200000000000000000000000000000000000006").unwrap(),
468 "WETH",
469 18,
470 0,
471 &[Some(2300)],
472 Chain::Base,
473 100,
474 )
475 }
476
477 fn base_usdc() -> Token {
478 Token::new(
479 &Bytes::from_str("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913").unwrap(),
480 "USDC",
481 6,
482 0,
483 &[Some(1)],
484 Chain::Base,
485 100,
486 )
487 }
488
489 fn state() -> MetricState {
490 let weth = weth();
491 let usdc = usdc();
492 let metadata = MetricMetadata {
493 pool_address: Bytes::from_str("0xbF48bCf474d57fF82A3215319229e0DE1476A557").unwrap(),
494 token0: weth.address.clone(),
495 token1: usdc.address.clone(),
496 tvl_fiat: Some(3000.0),
497 };
498 let bid_ask = MetricBidAskResponse {
499 bid_adj: big("55340232221128654848000"),
501 ask_adj: big("55524699661865750400000"),
503 total_token0_available: Some(big("10000000000000000000")),
504 total_token1_available: Some(big("30000000000")),
505 server_ts: 100,
506 price_provider_status: Some("healthy".to_string()),
507 depth: MetricDepth::default(),
508 };
509 let client = MetricClient::new(
510 Chain::Ethereum,
511 HashSet::new(),
512 0.0,
513 "http://localhost:8080".to_string(),
514 None,
515 Duration::from_secs(1),
516 Duration::from_secs(1),
517 )
518 .unwrap();
519 MetricState::new(weth, usdc, metadata, bid_ask, client)
520 }
521
522 #[test]
523 fn test_get_amount_out_zero_for_one() {
524 let state = state();
525 let result = state
526 .get_amount_out(
527 BigUint::from(1_000_000_000_000_000_000u128),
528 &state.base_token,
529 &state.quote_token,
530 )
531 .unwrap();
532
533 assert_eq!(result.amount, BigUint::from(3_000_000_000u64));
534 }
535
536 #[test]
537 fn test_get_amount_out_one_for_zero() {
538 let state = state();
539 let result = state
540 .get_amount_out(BigUint::from(3_010_000_000u64), &state.quote_token, &state.base_token)
541 .unwrap();
542
543 assert_eq!(result.amount, BigUint::from(1_000_000_000_000_000_000u128));
544 }
545
546 #[test]
547 fn test_get_amount_out_caps_to_available_liquidity() {
548 let mut state = state();
549 state.bid_ask.total_token1_available = Some(big("1500000000"));
550 let err = state
551 .get_amount_out(
552 BigUint::from(1_000_000_000_000_000_000u128),
553 &state.base_token,
554 &state.quote_token,
555 )
556 .unwrap_err();
557
558 assert!(matches!(err, SimulationError::InvalidInput(_, Some(_))));
559 }
560
561 #[test]
562 fn test_get_amount_out_depth_exhausted_reports_depth_message() {
563 let mut state = state();
564 state.bid_ask.depth.bids = vec![MetricDepthBin {
565 bin_idx: 0,
566 price: big("53495557813757699686400"),
568 cumulative_volume: big("3000000000"),
570 cumulative_input_volume: big("1016949152542372881"),
572 }];
573
574 let err = state
575 .get_amount_out(
576 BigUint::from(2_000_000_000_000_000_000u128),
578 &state.base_token,
579 &state.quote_token,
580 )
581 .unwrap_err();
582
583 match err {
584 SimulationError::InvalidInput(msg, Some(_)) => {
585 assert!(msg.contains("depth exhausted"), "unexpected message: {msg}");
586 }
587 other => panic!("expected InvalidInput, got {other:?}"),
588 }
589 }
590
591 #[test]
592 fn test_get_amount_out_exhausted_returns_exact_cap() {
593 let mut state = state();
594 let cap = "6575581573690662958";
597 state.bid_ask.depth.asks = vec![MetricDepthBin {
598 bin_idx: 0,
599 price: big("57184906628499610009600"),
601 cumulative_volume: big(cap),
602 cumulative_input_volume: big("20088000000"),
605 }];
606
607 let err = state
608 .get_amount_out(
609 BigUint::from(30_000_000_000u64),
611 &state.quote_token,
612 &state.base_token,
613 )
614 .unwrap_err();
615
616 match err {
617 SimulationError::InvalidInput(msg, Some(res)) => {
618 assert!(msg.contains("depth exhausted"), "unexpected message: {msg}");
619 assert_eq!(res.amount, BigUint::from_str(cap).unwrap());
621 }
622 other => panic!("expected InvalidInput, got {other:?}"),
623 }
624 }
625
626 #[test]
627 fn test_get_limits_caps_to_depth() {
628 let mut state = state();
629 state.bid_ask.depth.bids = vec![MetricDepthBin {
630 bin_idx: 0,
631 price: big("53495557813757699686400"),
633 cumulative_volume: big("1500000000"),
635 cumulative_input_volume: big("508474576271186440"),
637 }];
638
639 let (sell_limit, buy_limit) = state
640 .get_limits(state.base_token.address.clone(), state.quote_token.address.clone())
641 .unwrap();
642
643 assert_eq!(buy_limit, BigUint::from(1_500_000_000u64));
645 assert_eq!(sell_limit, BigUint::from(508_474_576_271_186_440u128));
648 }
649
650 #[test]
651 fn test_get_limits_aggregate_truncates_depth() {
652 let mut state = state();
653 state.bid_ask.total_token1_available = Some(big("1000000000"));
656 state.bid_ask.depth.bids = vec![MetricDepthBin {
657 bin_idx: 0,
658 price: big("53495557813757699686400"),
660 cumulative_volume: big("3000000000"),
661 cumulative_input_volume: big("1016949152542372881"),
662 }];
663
664 let (sell_limit, buy_limit) = state
665 .get_limits(state.base_token.address.clone(), state.quote_token.address.clone())
666 .unwrap();
667
668 assert_eq!(buy_limit, BigUint::from(1_000_000_000u64));
670 assert_eq!(sell_limit, BigUint::from(333_333_333_333_333_312u128));
673 }
674
675 #[test]
676 fn test_get_limits_uses_aggregate_without_depth() {
677 let state = state();
678
679 let (_, buy_limit) = state
680 .get_limits(state.base_token.address.clone(), state.quote_token.address.clone())
681 .unwrap();
682
683 assert_eq!(buy_limit, BigUint::from(30_000_000_000u64));
685 }
686
687 #[test]
688 fn test_get_amount_out_walks_bid_depth() {
689 let mut state = state();
690 state.bid_ask.depth.bids = vec![MetricDepthBin {
691 bin_idx: 0,
692 price: big("53495557813757699686400"),
694 cumulative_volume: big("3000000000"),
695 cumulative_input_volume: big("1016949152542372881"),
697 }];
698
699 let result = state
700 .get_amount_out(
701 BigUint::from(1_000_000_000_000_000_000u128),
702 &state.base_token,
703 &state.quote_token,
704 )
705 .unwrap();
706
707 assert_eq!(result.amount, BigUint::from(2_950_000_000u64));
710 }
711
712 #[test]
713 fn test_get_amount_out_walks_ask_depth() {
714 let mut state = state();
715 state.bid_ask.depth.asks = vec![MetricDepthBin {
716 bin_idx: 0,
717 price: big("57184906628499610009600"),
719 cumulative_volume: big("1000000000000000000"),
720 cumulative_input_volume: big("3055000000"),
722 }];
723
724 let result = state
725 .get_amount_out(BigUint::from(3_000_000_000u64), &state.quote_token, &state.base_token)
726 .unwrap();
727
728 assert!(result.amount < BigUint::from(1_000_000_000_000_000_000u128));
729 assert!(result.amount > BigUint::from(980_000_000_000_000_000u128));
730 }
731
732 fn depth_bin(cumulative_volume: &str, cumulative_input_volume: &str) -> MetricDepthBin {
733 MetricDepthBin {
734 bin_idx: 0,
735 price: big("53495557813757699686400"),
737 cumulative_volume: big(cumulative_volume),
738 cumulative_input_volume: big(cumulative_input_volume),
739 }
740 }
741
742 #[test]
743 fn test_depth_output_for_input_partially_fills_bid_bin() {
744 let bins = vec![depth_bin("3000000000", "1016949152542372881")];
746
747 let fill = depth_output_for_input(
748 &bins,
749 &BigUint::from(1_000_000_000_000_000_000u128),
750 &BigUint::from(3_000_000_000u64),
751 )
752 .unwrap();
753
754 assert_eq!(fill.output, BigUint::from(2_950_000_000u64));
756 assert!(!fill.exhausted);
757 }
758
759 #[test]
760 fn test_depth_output_for_input_partially_fills_ask_bin() {
761 let bins = vec![depth_bin("1000000000000000000", "3055000000")];
763
764 let fill = depth_output_for_input(
765 &bins,
766 &BigUint::from(3_000_000_000u64),
767 &BigUint::from(1_000_000_000_000_000_000u128),
768 )
769 .unwrap();
770
771 let expected = BigUint::from(1_000_000_000_000_000_000u128) *
773 BigUint::from(3_000_000_000u64) /
774 BigUint::from(3_055_000_000u64);
775 assert_eq!(fill.output, expected);
776 assert!(!fill.exhausted);
777 }
778
779 #[test]
780 fn test_depth_output_for_input_exhausts_available_depth() {
781 let bins = vec![depth_bin("3000000000", "1016949152542372881")];
782
783 let fill = depth_output_for_input(
784 &bins,
785 &BigUint::from(2_000_000_000_000_000_000u128),
786 &BigUint::from(3_000_000_000u64),
787 )
788 .unwrap();
789
790 assert_eq!(fill.output, BigUint::from(3_000_000_000u64));
791 assert!(fill.exhausted);
792 }
793
794 #[test]
795 fn test_depth_output_for_input_walks_multiple_bins() {
796 let bins = vec![
798 depth_bin("3000000000", "1000000000000000000"),
799 depth_bin("6000000000", "2100000000000000000"),
800 ];
801
802 let fill = depth_output_for_input(
803 &bins,
804 &BigUint::from(1_550_000_000_000_000_000u128),
806 &BigUint::from(6_000_000_000u64),
807 )
808 .unwrap();
809
810 assert_eq!(fill.output, BigUint::from(4_500_000_000u64));
812 assert!(!fill.exhausted);
813 }
814
815 #[test]
816 fn test_depth_output_for_input_caps_slice_to_aggregate_inventory() {
817 let bins = vec![depth_bin("3000000000", "1000000000000000000")];
818
819 let fill = depth_output_for_input(
822 &bins,
823 &BigUint::from(1_000_000_000_000_000_000u128),
824 &BigUint::from(1_500_000_000u64),
825 )
826 .unwrap();
827
828 assert_eq!(fill.output, BigUint::from(1_500_000_000u64));
829 assert!(fill.exhausted);
830 }
831
832 #[test]
833 fn test_depth_output_for_input_rejects_inconsistent_bin() {
834 let bins = vec![depth_bin("3000000000", "0")];
836
837 let err = depth_output_for_input(
838 &bins,
839 &BigUint::from(1_000_000_000_000_000_000u128),
840 &BigUint::from(3_000_000_000u64),
841 )
842 .unwrap_err();
843
844 assert!(matches!(err, SimulationError::RecoverableError(_)));
845 }
846
847 #[test]
848 fn test_depth_output_for_input_rejects_non_monotonic_bins() {
849 let bins = vec![
850 depth_bin("3000000000", "1000000000000000000"),
851 depth_bin("2000000000", "2000000000000000000"),
853 ];
854
855 let err = depth_output_for_input(
856 &bins,
857 &BigUint::from(2_000_000_000_000_000_000u128),
858 &BigUint::from(3_000_000_000u64),
859 )
860 .unwrap_err();
861
862 assert!(matches!(err, SimulationError::RecoverableError(_)));
863 }
864
865 #[tokio::test]
866 #[ignore = "hits Metric's public API"]
867 async fn test_live_metric_api_state_get_amount_out_and_signed_quote() {
868 use crate::rfq::protocols::metric::models::PaginatedMetadataResponse;
869
870 let weth = base_weth();
872 let usdc = base_usdc();
873 let config = crate::rfq::constants::get_metric_config();
874 let base_url = config
875 .base_url
876 .trim_end_matches('/')
877 .to_string();
878 let client = MetricClient::new(
879 Chain::Base,
880 HashSet::from([weth.address.clone(), usdc.address.clone()]),
881 0.0,
882 base_url.clone(),
883 config.api_key.clone(),
884 Duration::from_secs(1),
885 Duration::from_secs(5),
886 )
887 .unwrap();
888
889 let http_client = reqwest::Client::new();
890 let metadata: PaginatedMetadataResponse = http_client
891 .get(format!("{base_url}/public/v1/evm/8453/metadata"))
892 .header("accept", "application/json")
893 .query(&[("count", "500")])
894 .send()
895 .await
896 .unwrap()
897 .json()
898 .await
899 .unwrap();
900
901 let mut selected = None;
902 for pool in metadata
903 .data
904 .into_iter()
905 .filter(|pool| pool.token0 == weth.address && pool.token1 == usdc.address)
906 {
907 let checksummed =
908 alloy::primitives::Address::from_slice(&pool.pool_address).to_checksum(None);
909 let mut request = http_client
910 .get(format!("{base_url}/public/v1/evm/8453/{checksummed}/bid_ask"))
911 .header("accept", "application/json");
912 if let Some(api_key) = &config.api_key {
913 request = request.bearer_auth(api_key);
914 }
915 let bid_ask: MetricBidAskResponse = request
916 .send()
917 .await
918 .unwrap()
919 .json()
920 .await
921 .unwrap();
922 let has_enough_quote_liquidity = bid_ask
923 .total_token1_available()
924 .map(|available| available > BigUint::from(10u8))
925 .unwrap_or(false);
926 if bid_ask.is_quotable() && has_enough_quote_liquidity {
927 selected = Some((pool, bid_ask));
928 break;
929 }
930 }
931
932 let Some((metadata, bid_ask)) = selected else {
933 eprintln!("Metric live API returned no liquid Base WETH/USDC pool; skipping");
934 return;
935 };
936
937 let state = MetricState::new(weth, usdc, metadata, bid_ask, client);
938 assert!(state.bid_ask.is_quotable());
939
940 let amount_in = BigUint::from(1_000_000_000u64);
941 let indicative_quote = state
942 .get_amount_out(amount_in.clone(), &state.base_token, &state.quote_token)
943 .unwrap();
944 let trader = Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap();
945 let signed_quote = state
946 .request_signed_quote(GetAmountOutParams {
947 amount_in,
948 token_in: state.base_token.address.clone(),
949 token_out: state.quote_token.address.clone(),
950 sender: trader.clone(),
951 receiver: trader,
952 })
953 .await
954 .unwrap();
955
956 assert!(indicative_quote.amount > BigUint::from(0u8));
957 assert!(signed_quote.amount_out > BigUint::from(0u8));
958 assert_eq!(signed_quote.base_token, state.base_token.address);
959 assert_eq!(signed_quote.quote_token, state.quote_token.address);
960 assert!(signed_quote.quote_attributes.is_empty());
962 }
963}