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 = amount_out
200 .clone()
201 .min(effective_max_output.clone());
202 let res = GetAmountOutResult {
203 amount: capped_amount.clone(),
204 gas: BigUint::from(170_000u64),
205 new_state: self.clone_box(),
206 };
207
208 if exhausted || amount_out > effective_max_output {
209 return Err(SimulationError::InvalidInput(
210 format!(
211 "Metric pool has not enough liquidity. Requested output {}, available {}",
212 amount_out, effective_max_output
213 ),
214 Some(res),
215 ));
216 }
217
218 Ok(res)
219 }
220
221 fn get_limits(
222 &self,
223 sell_token: Bytes,
224 buy_token: Bytes,
225 ) -> Result<(BigUint, BigUint), SimulationError> {
226 let direction = self.direction(&sell_token, &buy_token)?;
227 match direction {
228 MetricDirection::ZeroForOne => {
229 let price = self.bid_ask.bid_price()?;
230 let buy_limit = self.bid_ask.total_token1_available()?;
231 let buy_limit_human = buy_limit.to_f64().ok_or_else(|| {
232 SimulationError::RecoverableError("Can't convert buy limit to f64".into())
233 })? / 10_f64.powi(self.quote_token.decimals as i32);
234 let sell_limit = buy_limit_human / price;
235 let sell_limit =
236 BigUint::from_f64(sell_limit * 10_f64.powi(self.base_token.decimals as i32))
237 .ok_or_else(|| {
238 SimulationError::RecoverableError(
239 "Can't convert sell limit to BigUint".into(),
240 )
241 })?;
242 Ok((sell_limit, buy_limit))
243 }
244 MetricDirection::OneForZero => {
245 let price = self.bid_ask.ask_price()?;
246 let buy_limit = self.bid_ask.total_token0_available()?;
247 let buy_limit_human = buy_limit.to_f64().ok_or_else(|| {
248 SimulationError::RecoverableError("Can't convert buy limit to f64".into())
249 })? / 10_f64.powi(self.base_token.decimals as i32);
250 let sell_limit = buy_limit_human * price;
251 let sell_limit =
252 BigUint::from_f64(sell_limit * 10_f64.powi(self.quote_token.decimals as i32))
253 .ok_or_else(|| {
254 SimulationError::RecoverableError(
255 "Can't convert sell limit to BigUint".into(),
256 )
257 })?;
258 Ok((sell_limit, buy_limit))
259 }
260 }
261 }
262
263 fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
264 Ok(self)
265 }
266
267 fn delta_transition(
268 &mut self,
269 _delta: ProtocolStateDelta,
270 _tokens: &HashMap<Bytes, Token>,
271 _balances: &Balances,
272 ) -> Result<(), TransitionError> {
273 Err(TransitionError::DecodeError(
275 "Metric RFQ state is snapshot-based and does not support deltas".into(),
276 ))
277 }
278
279 fn clone_box(&self) -> Box<dyn ProtocolSim> {
280 Box::new(self.clone())
281 }
282
283 fn as_any(&self) -> &dyn Any {
284 self
285 }
286
287 fn as_any_mut(&mut self) -> &mut dyn Any {
288 self
289 }
290
291 fn eq(&self, other: &dyn ProtocolSim) -> bool {
292 if let Some(other_state) = other
293 .as_any()
294 .downcast_ref::<MetricState>()
295 {
296 self.base_token == other_state.base_token &&
297 self.quote_token == other_state.quote_token &&
298 self.metadata == other_state.metadata &&
299 self.bid_ask == other_state.bid_ask
300 } else {
301 false
302 }
303 }
304}
305
306fn depth_max_output(bins: &[MetricDepthBin]) -> Result<Option<BigUint>, SimulationError> {
307 bins.last()
308 .map(|bin| {
309 bin.cumulative_volume()
310 .map_err(SimulationError::from)
311 })
312 .transpose()
313}
314
315fn depth_output_for_input(
316 direction: MetricDirection,
317 bins: &[MetricDepthBin],
318 start_price: f64,
319 input_human: f64,
320 output_decimals: u32,
321 max_output_human: f64,
322) -> Result<DepthFill, SimulationError> {
323 if input_human == 0.0 || max_output_human == 0.0 {
324 return Ok(DepthFill { output_human: 0.0, exhausted: input_human > 0.0 });
325 }
326
327 let mut current_price = start_price;
328 let mut previous_volume = 0.0;
329 let mut remaining_input = input_human;
330 let mut output_human = 0.0;
331
332 for bin in bins {
333 let bin_price = bin.price()?;
334 let cumulative =
337 raw_to_human(&bin.cumulative_volume()?, output_decimals, "depth cumulative volume")?;
338 let volume_in_bin = cumulative - previous_volume;
339 previous_volume = cumulative;
340
341 if volume_in_bin <= 0.0 {
342 current_price = bin_price;
343 continue;
344 }
345
346 let output_capacity = max_output_human - output_human;
347 if output_capacity <= 0.0 {
348 break;
349 }
350 let fillable_volume = volume_in_bin.min(output_capacity);
351 let segment_exit_price =
353 current_price + (bin_price - current_price) * fillable_volume / volume_in_bin;
354 let full_segment_input = depth_segment_input_required(
357 direction,
358 fillable_volume,
359 current_price,
360 segment_exit_price,
361 )?;
362
363 if remaining_input >= full_segment_input {
364 output_human += fillable_volume;
365 remaining_input -= full_segment_input;
366 current_price = segment_exit_price;
367 continue;
368 }
369
370 output_human += depth_segment_output_for_input(
374 direction,
375 remaining_input,
376 fillable_volume,
377 current_price,
378 segment_exit_price,
379 )?;
380 remaining_input = 0.0;
381 break;
382 }
383
384 if remaining_input > 1e-12 && output_human < max_output_human - 1e-12 {
385 return Err(SimulationError::RecoverableError(
386 "Metric depth has not enough cumulative volume".into(),
387 ));
388 }
389
390 Ok(DepthFill {
391 output_human: output_human.min(max_output_human),
392 exhausted: remaining_input > 1e-12,
393 })
394}
395
396fn depth_segment_input_required(
397 direction: MetricDirection,
398 output_human: f64,
399 entry_price: f64,
400 exit_price: f64,
401) -> Result<f64, SimulationError> {
402 let average_price = depth_segment_average_price(entry_price, exit_price)?;
403 match direction {
404 MetricDirection::ZeroForOne => Ok(output_human / average_price),
405 MetricDirection::OneForZero => Ok(output_human * average_price),
406 }
407}
408
409fn depth_segment_output_for_input(
410 direction: MetricDirection,
411 input_human: f64,
412 segment_output_human: f64,
413 entry_price: f64,
414 exit_price: f64,
415) -> Result<f64, SimulationError> {
416 if segment_output_human <= 0.0 || entry_price <= 0.0 {
417 return Err(SimulationError::RecoverableError(
418 "Metric depth has invalid price curve".into(),
419 ));
420 }
421
422 let average_slope = (exit_price - entry_price) / (2.0 * segment_output_human);
443 let output = match direction {
444 MetricDirection::ZeroForOne => {
445 let denominator = 1.0 - input_human * average_slope;
455 if denominator <= 0.0 {
456 return Err(SimulationError::RecoverableError(
457 "Metric depth has invalid price curve".into(),
458 ));
459 }
460 input_human * entry_price / denominator
461 }
462 MetricDirection::OneForZero => {
463 if average_slope.abs() < 1e-18 {
484 input_human / entry_price
485 } else {
486 let discriminant =
487 entry_price.mul_add(entry_price, 4.0 * average_slope * input_human);
488 if discriminant < 0.0 {
489 return Err(SimulationError::RecoverableError(
490 "Metric depth has invalid price curve".into(),
491 ));
492 }
493 let root_a = (-entry_price + discriminant.sqrt()) / (2.0 * average_slope);
494 let root_b = (-entry_price - discriminant.sqrt()) / (2.0 * average_slope);
495 [root_a, root_b]
496 .into_iter()
497 .find(|root| {
498 root.is_finite() && *root >= -1e-12 && *root <= segment_output_human + 1e-12
499 })
500 .ok_or_else(|| {
501 SimulationError::RecoverableError(
502 "Metric depth has invalid price curve".into(),
503 )
504 })?
505 }
506 }
507 };
508
509 Ok(output.clamp(0.0, segment_output_human))
510}
511
512fn depth_segment_average_price(entry_price: f64, exit_price: f64) -> Result<f64, SimulationError> {
513 let average_price = (entry_price + exit_price) / 2.0;
514 if average_price <= 0.0 {
515 return Err(SimulationError::RecoverableError(
516 "Metric depth has non-positive average price".into(),
517 ));
518 }
519 Ok(average_price)
520}
521
522fn raw_to_human(amount: &BigUint, decimals: u32, field: &str) -> Result<f64, SimulationError> {
523 let amount = amount.to_f64().ok_or_else(|| {
524 SimulationError::RecoverableError(format!("Can't convert {field} to f64"))
525 })?;
526 Ok(amount / 10_f64.powi(decimals as i32))
527}
528
529#[async_trait]
530impl IndicativelyPriced for MetricState {
531 async fn request_signed_quote(
532 &self,
533 params: GetAmountOutParams,
534 ) -> Result<SignedQuote, SimulationError> {
535 let direction = self.direction(¶ms.token_in, ¶ms.token_out)?;
536 let (token_in, token_out) = match direction {
537 MetricDirection::ZeroForOne => (&self.base_token, &self.quote_token),
538 MetricDirection::OneForZero => (&self.quote_token, &self.base_token),
539 };
540 let amount_out = self
541 .get_amount_out(params.amount_in.clone(), token_in, token_out)?
542 .amount;
543
544 Ok(self
547 .client
548 .request_oracle_update_for_pool(&self.metadata, ¶ms, amount_out)
549 .await?)
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use std::{collections::HashSet, str::FromStr};
556
557 use tokio::time::Duration;
558 use tycho_common::models::Chain;
559
560 use super::*;
561 use crate::rfq::protocols::metric::{client::MetricClient, models::MetricDepth};
562
563 fn weth() -> Token {
564 Token::new(
565 &Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
566 "WETH",
567 18,
568 0,
569 &[Some(2300)],
570 Chain::Ethereum,
571 100,
572 )
573 }
574
575 fn usdc() -> Token {
576 Token::new(
577 &Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
578 "USDC",
579 6,
580 0,
581 &[Some(1)],
582 Chain::Ethereum,
583 100,
584 )
585 }
586
587 fn state() -> MetricState {
588 let weth = weth();
589 let usdc = usdc();
590 let metadata = MetricMetadata {
591 pool_address: Bytes::from_str("0xbF48bCf474d57fF82A3215319229e0DE1476A557").unwrap(),
592 token0: weth.address.clone(),
593 token1: usdc.address.clone(),
594 };
595 let bid_ask = MetricBidAskResponse {
596 bid_adj: "55340232221128654848000".to_string(),
598 ask_adj: "55524699661865750400000".to_string(),
600 quote_available: true,
601 total_token0_available: "10000000000000000000".to_string(),
602 total_token1_available: "30000000000".to_string(),
603 latest_block: 100,
604 depth: MetricDepth::default(),
605 };
606 let client = MetricClient::new(
607 Chain::Ethereum,
608 HashSet::new(),
609 0.0,
610 HashSet::new(),
611 "http://localhost:8080".to_string(),
612 None,
613 Duration::from_secs(1),
614 Duration::from_secs(1),
615 )
616 .unwrap();
617 MetricState::new(weth, usdc, metadata, bid_ask, client)
618 }
619
620 #[test]
621 fn test_get_amount_out_zero_for_one() {
622 let state = state();
623 let result = state
624 .get_amount_out(
625 BigUint::from(1_000_000_000_000_000_000u128),
626 &state.base_token,
627 &state.quote_token,
628 )
629 .unwrap();
630
631 assert_eq!(result.amount, BigUint::from(3_000_000_000u64));
632 }
633
634 #[test]
635 fn test_get_amount_out_one_for_zero() {
636 let state = state();
637 let result = state
638 .get_amount_out(BigUint::from(3_010_000_000u64), &state.quote_token, &state.base_token)
639 .unwrap();
640
641 assert_eq!(result.amount, BigUint::from(1_000_000_000_000_000_000u128));
642 }
643
644 #[test]
645 fn test_get_amount_out_caps_to_available_liquidity() {
646 let mut state = state();
647 state.bid_ask.total_token1_available = "1500000000".to_string();
648 let err = state
649 .get_amount_out(
650 BigUint::from(1_000_000_000_000_000_000u128),
651 &state.base_token,
652 &state.quote_token,
653 )
654 .unwrap_err();
655
656 assert!(matches!(err, SimulationError::InvalidInput(_, Some(_))));
657 }
658
659 #[test]
660 fn test_get_amount_out_walks_bid_depth() {
661 let mut state = state();
662 state.bid_ask.depth.bids = vec![MetricDepthBin {
663 bin_idx: 0,
664 price: "53495557813757699686400".to_string(),
666 cumulative_volume: "3000000000".to_string(),
667 }];
668
669 let result = state
670 .get_amount_out(
671 BigUint::from(1_000_000_000_000_000_000u128),
672 &state.base_token,
673 &state.quote_token,
674 )
675 .unwrap();
676
677 assert!(result.amount < BigUint::from(3_000_000_000u64));
678 assert!(result.amount > BigUint::from(2_950_000_000u64));
679 }
680
681 #[test]
682 fn test_get_amount_out_walks_ask_depth() {
683 let mut state = state();
684 state.bid_ask.depth.asks = vec![MetricDepthBin {
685 bin_idx: 0,
686 price: "57184906628499610009600".to_string(),
688 cumulative_volume: "1000000000000000000".to_string(),
689 }];
690
691 let result = state
692 .get_amount_out(BigUint::from(3_000_000_000u64), &state.quote_token, &state.base_token)
693 .unwrap();
694
695 assert!(result.amount < BigUint::from(1_000_000_000_000_000_000u128));
696 assert!(result.amount > BigUint::from(980_000_000_000_000_000u128));
697 }
698
699 #[test]
700 fn test_depth_output_for_input_partially_fills_bid_bin() {
701 let bins = vec![MetricDepthBin {
702 bin_idx: 0,
703 price: "53495557813757699686400".to_string(),
705 cumulative_volume: "3000000000".to_string(),
706 }];
707
708 let fill =
709 depth_output_for_input(MetricDirection::ZeroForOne, &bins, 3000.0, 1.0, 6, 3000.0)
710 .unwrap();
711
712 assert!((fill.output_human - 2950.8196721311474).abs() < 1e-9);
713 assert!(!fill.exhausted);
714 }
715
716 #[test]
717 fn test_depth_output_for_input_partially_fills_ask_bin() {
718 let bins = vec![MetricDepthBin {
719 bin_idx: 0,
720 price: "57184906628499610009600".to_string(),
722 cumulative_volume: "1000000000000000000".to_string(),
723 }];
724
725 let fill =
726 depth_output_for_input(MetricDirection::OneForZero, &bins, 3010.0, 3000.0, 18, 1.0)
727 .unwrap();
728
729 assert!((fill.output_human - 0.9822534928279816).abs() < 1e-12);
730 assert!(!fill.exhausted);
731 }
732
733 #[test]
734 fn test_depth_output_for_input_exhausts_available_depth() {
735 let bins = vec![MetricDepthBin {
736 bin_idx: 0,
737 price: "53495557813757699686400".to_string(),
739 cumulative_volume: "3000000000".to_string(),
740 }];
741
742 let fill =
743 depth_output_for_input(MetricDirection::ZeroForOne, &bins, 3000.0, 2.0, 6, 3000.0)
744 .unwrap();
745
746 assert_eq!(fill.output_human, 3000.0);
747 assert!(fill.exhausted);
748 }
749
750 #[tokio::test]
751 #[ignore = "hits Metric's public API"]
752 async fn test_live_metric_api_state_get_amount_out_and_oracle_update() {
753 let weth = weth();
754 let usdc = usdc();
755 let base_url = std::env::var("METRIC_API_URL")
756 .unwrap_or_else(|_| "http://54.199.103.16:8080".to_string())
757 .trim_end_matches('/')
758 .to_string();
759 let client = MetricClient::new(
760 Chain::Ethereum,
761 HashSet::from([weth.address.clone(), usdc.address.clone()]),
762 0.0,
763 HashSet::new(),
764 base_url.clone(),
765 None,
766 Duration::from_secs(1),
767 Duration::from_secs(5),
768 )
769 .unwrap();
770
771 let http_client = reqwest::Client::new();
772 let metadata: Vec<MetricMetadata> = http_client
773 .get(format!("{base_url}/ethereum/metadata"))
774 .header("accept", "application/json")
775 .send()
776 .await
777 .unwrap()
778 .json()
779 .await
780 .unwrap();
781
782 let mut selected = None;
783 for pool in metadata
784 .into_iter()
785 .filter(|pool| pool.token0 == weth.address && pool.token1 == usdc.address)
786 {
787 let bid_ask: MetricBidAskResponse = http_client
788 .get(format!("{base_url}/ethereum/{}/bid_ask", pool.pool_address))
789 .header("accept", "application/json")
790 .send()
791 .await
792 .unwrap()
793 .json()
794 .await
795 .unwrap();
796 let has_enough_quote_liquidity = bid_ask
797 .total_token1_available()
798 .map(|available| available > BigUint::from(10u8))
799 .unwrap_or(false);
800 if bid_ask.quote_available && has_enough_quote_liquidity {
801 selected = Some((pool, bid_ask));
802 break;
803 }
804 }
805
806 let Some((metadata, bid_ask)) = selected else {
807 eprintln!("Metric live API returned no liquid Ethereum WETH/USDC pool; skipping");
808 return;
809 };
810
811 let state = MetricState::new(weth, usdc, metadata, bid_ask, client);
812 assert!(state.bid_ask.quote_available);
813
814 let amount_in = BigUint::from(1_000_000_000u64);
815 let indicative_quote = state
816 .get_amount_out(amount_in.clone(), &state.base_token, &state.quote_token)
817 .unwrap();
818 let trader = Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap();
819 let signed_quote = state
820 .request_signed_quote(GetAmountOutParams {
821 amount_in,
822 token_in: state.base_token.address.clone(),
823 token_out: state.quote_token.address.clone(),
824 sender: trader.clone(),
825 receiver: trader,
826 })
827 .await
828 .unwrap();
829
830 assert!(indicative_quote.amount > BigUint::from(0u8));
831 assert!(signed_quote.amount_out > BigUint::from(0u8));
832 assert_eq!(signed_quote.base_token, state.base_token.address);
833 assert_eq!(signed_quote.quote_token, state.quote_token.address);
834 assert!(!signed_quote.quote_attributes["oracle_update_0_args"].is_empty());
835 }
836}