use std::{any::Any, collections::HashMap, fmt};
use async_trait::async_trait;
use num_bigint::BigUint;
use num_traits::{FromPrimitive, ToPrimitive, Zero};
use serde::{Deserialize, Serialize};
use tycho_common::{
dto::ProtocolStateDelta,
models::{protocol::GetAmountOutParams, token::Token},
simulation::{
errors::{SimulationError, TransitionError},
indicatively_priced::{IndicativelyPriced, SignedQuote},
protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
},
Bytes,
};
use crate::rfq::protocols::metric::{
client::MetricClient,
models::{MetricBidAskResponse, MetricDepthBin, MetricMetadata},
};
const METRIC_SWAP_GAS: u64 = 170_000;
#[derive(Clone, Serialize, Deserialize)]
pub struct MetricState {
pub base_token: Token,
pub quote_token: Token,
pub metadata: MetricMetadata,
pub bid_ask: MetricBidAskResponse,
pub client: MetricClient,
}
impl fmt::Debug for MetricState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MetricState")
.field("base_token", &self.base_token)
.field("quote_token", &self.quote_token)
.field("pool", &self.metadata.pool_address)
.field("server_ts", &self.bid_ask.server_ts)
.finish_non_exhaustive()
}
}
impl MetricState {
pub fn new(
base_token: Token,
quote_token: Token,
metadata: MetricMetadata,
bid_ask: MetricBidAskResponse,
client: MetricClient,
) -> Self {
Self { base_token, quote_token, metadata, bid_ask, client }
}
fn direction(
&self,
token_in: &Bytes,
token_out: &Bytes,
) -> Result<MetricDirection, SimulationError> {
if token_in == &self.base_token.address && token_out == &self.quote_token.address {
Ok(MetricDirection::ZeroForOne)
} else if token_in == &self.quote_token.address && token_out == &self.base_token.address {
Ok(MetricDirection::OneForZero)
} else {
Err(SimulationError::InvalidInput(
format!(
"Invalid token addresses. Got in={token_in}, out={token_out}, expected {} / {}",
self.base_token.address, self.quote_token.address
),
None,
))
}
}
fn quote_with_depth(
&self,
direction: MetricDirection,
amount_in: &BigUint,
max_output: &BigUint,
) -> Result<Option<DepthQuote>, SimulationError> {
let bins = match direction {
MetricDirection::ZeroForOne => &self.bid_ask.depth.bids,
MetricDirection::OneForZero => &self.bid_ask.depth.asks,
};
let Some(depth_max_output) = depth_max_output(bins) else {
return Ok(None);
};
let effective_max_output = depth_max_output.min(max_output.clone());
let depth_fill = depth_output_for_input(bins, amount_in, &effective_max_output)?;
Ok(Some(DepthQuote {
amount_out: depth_fill.output,
max_output: effective_max_output,
exhausted: depth_fill.exhausted,
}))
}
}
#[derive(Debug, Clone, Copy)]
enum MetricDirection {
ZeroForOne,
OneForZero,
}
struct DepthQuote {
amount_out: BigUint,
max_output: BigUint,
exhausted: bool,
}
#[derive(Debug)]
struct DepthFill {
output: BigUint,
exhausted: bool,
}
#[typetag::serde]
impl ProtocolSim for MetricState {
fn fee(&self) -> f64 {
0.0
}
fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
let bid = self.bid_ask.bid_price()?;
let ask = self.bid_ask.ask_price()?;
let mid = (bid + ask) / 2.0;
if base.address == self.base_token.address && quote.address == self.quote_token.address {
Ok(mid)
} else if base.address == self.quote_token.address &&
quote.address == self.base_token.address
{
Ok(1.0 / mid)
} else {
Err(SimulationError::InvalidInput(
format!(
"Invalid token addresses. Got base={}, quote={}, expected {} / {}",
base.address, quote.address, self.base_token.address, self.quote_token.address
),
None,
))
}
}
fn get_amount_out(
&self,
amount_in: BigUint,
token_in: &Token,
token_out: &Token,
) -> Result<GetAmountOutResult, SimulationError> {
let direction = self.direction(&token_in.address, &token_out.address)?;
let max_output = match direction {
MetricDirection::ZeroForOne => self.bid_ask.total_token1_available()?,
MetricDirection::OneForZero => self.bid_ask.total_token0_available()?,
};
if let Some(quote) = self.quote_with_depth(direction, &amount_in, &max_output)? {
let res = GetAmountOutResult {
amount: quote.amount_out,
gas: BigUint::from(METRIC_SWAP_GAS),
new_state: self.clone_box(),
};
if quote.exhausted {
return Err(SimulationError::InvalidInput(
format!(
"Metric pool depth exhausted. Input {amount_in} cannot be fully filled; \
tradable depth caps output at {}",
quote.max_output
),
Some(res),
));
}
return Ok(res);
}
let amount_in_human = amount_in.to_f64().ok_or_else(|| {
SimulationError::RecoverableError("Can't convert amount in to f64".into())
})? / 10_f64.powi(token_in.decimals as i32);
let flat_amount_out_human = match direction {
MetricDirection::ZeroForOne => amount_in_human * self.bid_ask.bid_price()?,
MetricDirection::OneForZero => amount_in_human / self.bid_ask.ask_price()?,
};
let amount_out =
BigUint::from_f64(flat_amount_out_human * 10_f64.powi(token_out.decimals as i32))
.ok_or_else(|| {
SimulationError::RecoverableError("Can't convert amount out to BigUint".into())
})?;
let res = GetAmountOutResult {
amount: amount_out
.clone()
.min(max_output.clone()),
gas: BigUint::from(METRIC_SWAP_GAS),
new_state: self.clone_box(),
};
if amount_out > max_output {
return Err(SimulationError::InvalidInput(
format!(
"Metric pool has not enough liquidity. Requested output {amount_out} exceeds \
available {max_output}"
),
Some(res),
));
}
Ok(res)
}
fn get_limits(
&self,
sell_token: Bytes,
buy_token: Bytes,
) -> Result<(BigUint, BigUint), SimulationError> {
let direction = self.direction(&sell_token, &buy_token)?;
let (sell_per_buy, aggregate, bins, sell_decimals, buy_decimals) = match direction {
MetricDirection::ZeroForOne => (
1.0 / self.bid_ask.bid_price()?,
self.bid_ask.total_token1_available()?,
&self.bid_ask.depth.bids,
self.base_token.decimals,
self.quote_token.decimals,
),
MetricDirection::OneForZero => (
self.bid_ask.ask_price()?,
self.bid_ask.total_token0_available()?,
&self.bid_ask.depth.asks,
self.quote_token.decimals,
self.base_token.decimals,
),
};
if let Some(last_bin) = bins.last() {
if last_bin.cumulative_volume <= aggregate {
return Ok((
last_bin.cumulative_input_volume.clone(),
last_bin.cumulative_volume.clone(),
));
}
}
let buy_limit = aggregate;
let buy_limit_human = buy_limit.to_f64().ok_or_else(|| {
SimulationError::RecoverableError("Can't convert buy limit to f64".into())
})? / 10_f64.powi(buy_decimals as i32);
let sell_limit =
BigUint::from_f64(buy_limit_human * sell_per_buy * 10_f64.powi(sell_decimals as i32))
.ok_or_else(|| {
SimulationError::RecoverableError("Can't convert sell limit to BigUint".into())
})?;
Ok((sell_limit, buy_limit))
}
fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
Ok(self)
}
fn delta_transition(
&mut self,
_delta: ProtocolStateDelta,
_tokens: &HashMap<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError> {
Err(TransitionError::DecodeError(
"Metric RFQ state is snapshot-based and does not support deltas".into(),
))
}
fn clone_box(&self) -> Box<dyn ProtocolSim> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn eq(&self, other: &dyn ProtocolSim) -> bool {
if let Some(other_state) = other
.as_any()
.downcast_ref::<MetricState>()
{
self.base_token == other_state.base_token &&
self.quote_token == other_state.quote_token &&
self.metadata == other_state.metadata &&
self.bid_ask == other_state.bid_ask
} else {
false
}
}
}
fn depth_max_output(bins: &[MetricDepthBin]) -> Option<BigUint> {
bins.last()
.map(|bin| bin.cumulative_volume.clone())
}
fn depth_output_for_input(
bins: &[MetricDepthBin],
amount_in: &BigUint,
max_output: &BigUint,
) -> Result<DepthFill, SimulationError> {
if amount_in.is_zero() || max_output.is_zero() {
return Ok(DepthFill { output: BigUint::ZERO, exhausted: !amount_in.is_zero() });
}
let mut previous_output = BigUint::ZERO;
let mut previous_input = BigUint::ZERO;
let mut remaining_input = amount_in.clone();
let mut output = BigUint::ZERO;
for bin in bins {
let cumulative_output = &bin.cumulative_volume;
let cumulative_input = &bin.cumulative_input_volume;
if cumulative_output < &previous_output || cumulative_input < &previous_input {
return Err(SimulationError::RecoverableError(
"Metric depth cumulative volumes are not monotonic".into(),
));
}
let volume_in_bin = cumulative_output - &previous_output;
let input_in_bin = cumulative_input - &previous_input;
previous_output = cumulative_output.clone();
previous_input = cumulative_input.clone();
if volume_in_bin.is_zero() && input_in_bin.is_zero() {
continue;
}
if volume_in_bin.is_zero() || input_in_bin.is_zero() {
return Err(SimulationError::RecoverableError(
"Metric depth bin has inconsistent volume and input".into(),
));
}
let output_capacity = max_output - &output;
if output_capacity.is_zero() {
break;
}
let (fillable_volume, fillable_input) = if volume_in_bin <= output_capacity {
(volume_in_bin, input_in_bin)
} else {
let fillable_input = (&input_in_bin * &output_capacity + &volume_in_bin -
BigUint::from(1u8)) /
&volume_in_bin;
(output_capacity, fillable_input)
};
if remaining_input >= fillable_input {
output += &fillable_volume;
remaining_input -= &fillable_input;
continue;
}
output += &fillable_volume * &remaining_input / &fillable_input;
remaining_input = BigUint::ZERO;
break;
}
Ok(DepthFill { output, exhausted: !remaining_input.is_zero() })
}
#[async_trait]
impl IndicativelyPriced for MetricState {
async fn request_signed_quote(
&self,
params: GetAmountOutParams,
) -> Result<SignedQuote, SimulationError> {
let direction = self.direction(¶ms.token_in, ¶ms.token_out)?;
let (token_in, token_out) = match direction {
MetricDirection::ZeroForOne => (&self.base_token, &self.quote_token),
MetricDirection::OneForZero => (&self.quote_token, &self.base_token),
};
let amount_out = self
.get_amount_out(params.amount_in.clone(), token_in, token_out)?
.amount;
Ok(SignedQuote {
base_token: params.token_in.clone(),
quote_token: params.token_out.clone(),
amount_in: params.amount_in.clone(),
amount_out,
quote_attributes: HashMap::new(),
})
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashSet, str::FromStr};
use tokio::time::Duration;
use tycho_common::models::Chain;
use super::*;
use crate::rfq::protocols::metric::{client::MetricClient, models::MetricDepth};
fn big(value: &str) -> BigUint {
value.parse().unwrap()
}
fn weth() -> Token {
Token::new(
&Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
"WETH",
18,
0,
&[Some(2300)],
Chain::Ethereum,
100,
)
}
fn usdc() -> Token {
Token::new(
&Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
"USDC",
6,
0,
&[Some(1)],
Chain::Ethereum,
100,
)
}
fn base_weth() -> Token {
Token::new(
&Bytes::from_str("0x4200000000000000000000000000000000000006").unwrap(),
"WETH",
18,
0,
&[Some(2300)],
Chain::Base,
100,
)
}
fn base_usdc() -> Token {
Token::new(
&Bytes::from_str("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913").unwrap(),
"USDC",
6,
0,
&[Some(1)],
Chain::Base,
100,
)
}
fn state() -> MetricState {
let weth = weth();
let usdc = usdc();
let metadata = MetricMetadata {
pool_address: Bytes::from_str("0xbF48bCf474d57fF82A3215319229e0DE1476A557").unwrap(),
token0: weth.address.clone(),
token1: usdc.address.clone(),
tvl_fiat: Some(3000.0),
};
let bid_ask = MetricBidAskResponse {
bid_adj: big("55340232221128654848000"),
ask_adj: big("55524699661865750400000"),
total_token0_available: Some(big("10000000000000000000")),
total_token1_available: Some(big("30000000000")),
server_ts: 100,
price_provider_status: Some("healthy".to_string()),
depth: MetricDepth::default(),
};
let client = MetricClient::new(
Chain::Ethereum,
HashSet::new(),
0.0,
"http://localhost:8080".to_string(),
None,
Duration::from_secs(1),
Duration::from_secs(1),
)
.unwrap();
MetricState::new(weth, usdc, metadata, bid_ask, client)
}
#[test]
fn test_get_amount_out_zero_for_one() {
let state = state();
let result = state
.get_amount_out(
BigUint::from(1_000_000_000_000_000_000u128),
&state.base_token,
&state.quote_token,
)
.unwrap();
assert_eq!(result.amount, BigUint::from(3_000_000_000u64));
}
#[test]
fn test_get_amount_out_one_for_zero() {
let state = state();
let result = state
.get_amount_out(BigUint::from(3_010_000_000u64), &state.quote_token, &state.base_token)
.unwrap();
assert_eq!(result.amount, BigUint::from(1_000_000_000_000_000_000u128));
}
#[test]
fn test_get_amount_out_caps_to_available_liquidity() {
let mut state = state();
state.bid_ask.total_token1_available = Some(big("1500000000"));
let err = state
.get_amount_out(
BigUint::from(1_000_000_000_000_000_000u128),
&state.base_token,
&state.quote_token,
)
.unwrap_err();
assert!(matches!(err, SimulationError::InvalidInput(_, Some(_))));
}
#[test]
fn test_get_amount_out_depth_exhausted_reports_depth_message() {
let mut state = state();
state.bid_ask.depth.bids = vec![MetricDepthBin {
bin_idx: 0,
price: big("53495557813757699686400"),
cumulative_volume: big("3000000000"),
cumulative_input_volume: big("1016949152542372881"),
}];
let err = state
.get_amount_out(
BigUint::from(2_000_000_000_000_000_000u128),
&state.base_token,
&state.quote_token,
)
.unwrap_err();
match err {
SimulationError::InvalidInput(msg, Some(_)) => {
assert!(msg.contains("depth exhausted"), "unexpected message: {msg}");
}
other => panic!("expected InvalidInput, got {other:?}"),
}
}
#[test]
fn test_get_amount_out_exhausted_returns_exact_cap() {
let mut state = state();
let cap = "6575581573690662958";
state.bid_ask.depth.asks = vec![MetricDepthBin {
bin_idx: 0,
price: big("57184906628499610009600"),
cumulative_volume: big(cap),
cumulative_input_volume: big("20088000000"),
}];
let err = state
.get_amount_out(
BigUint::from(30_000_000_000u64),
&state.quote_token,
&state.base_token,
)
.unwrap_err();
match err {
SimulationError::InvalidInput(msg, Some(res)) => {
assert!(msg.contains("depth exhausted"), "unexpected message: {msg}");
assert_eq!(res.amount, BigUint::from_str(cap).unwrap());
}
other => panic!("expected InvalidInput, got {other:?}"),
}
}
#[test]
fn test_get_limits_caps_to_depth() {
let mut state = state();
state.bid_ask.depth.bids = vec![MetricDepthBin {
bin_idx: 0,
price: big("53495557813757699686400"),
cumulative_volume: big("1500000000"),
cumulative_input_volume: big("508474576271186440"),
}];
let (sell_limit, buy_limit) = state
.get_limits(state.base_token.address.clone(), state.quote_token.address.clone())
.unwrap();
assert_eq!(buy_limit, BigUint::from(1_500_000_000u64));
assert_eq!(sell_limit, BigUint::from(508_474_576_271_186_440u128));
}
#[test]
fn test_get_limits_aggregate_truncates_depth() {
let mut state = state();
state.bid_ask.total_token1_available = Some(big("1000000000"));
state.bid_ask.depth.bids = vec![MetricDepthBin {
bin_idx: 0,
price: big("53495557813757699686400"),
cumulative_volume: big("3000000000"),
cumulative_input_volume: big("1016949152542372881"),
}];
let (sell_limit, buy_limit) = state
.get_limits(state.base_token.address.clone(), state.quote_token.address.clone())
.unwrap();
assert_eq!(buy_limit, BigUint::from(1_000_000_000u64));
assert_eq!(sell_limit, BigUint::from(333_333_333_333_333_312u128));
}
#[test]
fn test_get_limits_uses_aggregate_without_depth() {
let state = state();
let (_, buy_limit) = state
.get_limits(state.base_token.address.clone(), state.quote_token.address.clone())
.unwrap();
assert_eq!(buy_limit, BigUint::from(30_000_000_000u64));
}
#[test]
fn test_get_amount_out_walks_bid_depth() {
let mut state = state();
state.bid_ask.depth.bids = vec![MetricDepthBin {
bin_idx: 0,
price: big("53495557813757699686400"),
cumulative_volume: big("3000000000"),
cumulative_input_volume: big("1016949152542372881"),
}];
let result = state
.get_amount_out(
BigUint::from(1_000_000_000_000_000_000u128),
&state.base_token,
&state.quote_token,
)
.unwrap();
assert_eq!(result.amount, BigUint::from(2_950_000_000u64));
}
#[test]
fn test_get_amount_out_walks_ask_depth() {
let mut state = state();
state.bid_ask.depth.asks = vec![MetricDepthBin {
bin_idx: 0,
price: big("57184906628499610009600"),
cumulative_volume: big("1000000000000000000"),
cumulative_input_volume: big("3055000000"),
}];
let result = state
.get_amount_out(BigUint::from(3_000_000_000u64), &state.quote_token, &state.base_token)
.unwrap();
assert!(result.amount < BigUint::from(1_000_000_000_000_000_000u128));
assert!(result.amount > BigUint::from(980_000_000_000_000_000u128));
}
fn depth_bin(cumulative_volume: &str, cumulative_input_volume: &str) -> MetricDepthBin {
MetricDepthBin {
bin_idx: 0,
price: big("53495557813757699686400"),
cumulative_volume: big(cumulative_volume),
cumulative_input_volume: big(cumulative_input_volume),
}
}
#[test]
fn test_depth_output_for_input_partially_fills_bid_bin() {
let bins = vec![depth_bin("3000000000", "1016949152542372881")];
let fill = depth_output_for_input(
&bins,
&BigUint::from(1_000_000_000_000_000_000u128),
&BigUint::from(3_000_000_000u64),
)
.unwrap();
assert_eq!(fill.output, BigUint::from(2_950_000_000u64));
assert!(!fill.exhausted);
}
#[test]
fn test_depth_output_for_input_partially_fills_ask_bin() {
let bins = vec![depth_bin("1000000000000000000", "3055000000")];
let fill = depth_output_for_input(
&bins,
&BigUint::from(3_000_000_000u64),
&BigUint::from(1_000_000_000_000_000_000u128),
)
.unwrap();
let expected = BigUint::from(1_000_000_000_000_000_000u128) *
BigUint::from(3_000_000_000u64) /
BigUint::from(3_055_000_000u64);
assert_eq!(fill.output, expected);
assert!(!fill.exhausted);
}
#[test]
fn test_depth_output_for_input_exhausts_available_depth() {
let bins = vec![depth_bin("3000000000", "1016949152542372881")];
let fill = depth_output_for_input(
&bins,
&BigUint::from(2_000_000_000_000_000_000u128),
&BigUint::from(3_000_000_000u64),
)
.unwrap();
assert_eq!(fill.output, BigUint::from(3_000_000_000u64));
assert!(fill.exhausted);
}
#[test]
fn test_depth_output_for_input_walks_multiple_bins() {
let bins = vec![
depth_bin("3000000000", "1000000000000000000"),
depth_bin("6000000000", "2100000000000000000"),
];
let fill = depth_output_for_input(
&bins,
&BigUint::from(1_550_000_000_000_000_000u128),
&BigUint::from(6_000_000_000u64),
)
.unwrap();
assert_eq!(fill.output, BigUint::from(4_500_000_000u64));
assert!(!fill.exhausted);
}
#[test]
fn test_depth_output_for_input_caps_slice_to_aggregate_inventory() {
let bins = vec![depth_bin("3000000000", "1000000000000000000")];
let fill = depth_output_for_input(
&bins,
&BigUint::from(1_000_000_000_000_000_000u128),
&BigUint::from(1_500_000_000u64),
)
.unwrap();
assert_eq!(fill.output, BigUint::from(1_500_000_000u64));
assert!(fill.exhausted);
}
#[test]
fn test_depth_output_for_input_rejects_inconsistent_bin() {
let bins = vec![depth_bin("3000000000", "0")];
let err = depth_output_for_input(
&bins,
&BigUint::from(1_000_000_000_000_000_000u128),
&BigUint::from(3_000_000_000u64),
)
.unwrap_err();
assert!(matches!(err, SimulationError::RecoverableError(_)));
}
#[test]
fn test_depth_output_for_input_rejects_non_monotonic_bins() {
let bins = vec![
depth_bin("3000000000", "1000000000000000000"),
depth_bin("2000000000", "2000000000000000000"),
];
let err = depth_output_for_input(
&bins,
&BigUint::from(2_000_000_000_000_000_000u128),
&BigUint::from(3_000_000_000u64),
)
.unwrap_err();
assert!(matches!(err, SimulationError::RecoverableError(_)));
}
#[tokio::test]
#[ignore = "hits Metric's public API"]
async fn test_live_metric_api_state_get_amount_out_and_signed_quote() {
use crate::rfq::protocols::metric::models::PaginatedMetadataResponse;
let weth = base_weth();
let usdc = base_usdc();
let config = crate::rfq::constants::get_metric_config();
let base_url = config
.base_url
.trim_end_matches('/')
.to_string();
let client = MetricClient::new(
Chain::Base,
HashSet::from([weth.address.clone(), usdc.address.clone()]),
0.0,
base_url.clone(),
config.api_key.clone(),
Duration::from_secs(1),
Duration::from_secs(5),
)
.unwrap();
let http_client = reqwest::Client::new();
let metadata: PaginatedMetadataResponse = http_client
.get(format!("{base_url}/public/v1/evm/8453/metadata"))
.header("accept", "application/json")
.query(&[("count", "500")])
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let mut selected = None;
for pool in metadata
.data
.into_iter()
.filter(|pool| pool.token0 == weth.address && pool.token1 == usdc.address)
{
let checksummed =
alloy::primitives::Address::from_slice(&pool.pool_address).to_checksum(None);
let mut request = http_client
.get(format!("{base_url}/public/v1/evm/8453/{checksummed}/bid_ask"))
.header("accept", "application/json");
if let Some(api_key) = &config.api_key {
request = request.bearer_auth(api_key);
}
let bid_ask: MetricBidAskResponse = request
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let has_enough_quote_liquidity = bid_ask
.total_token1_available()
.map(|available| available > BigUint::from(10u8))
.unwrap_or(false);
if bid_ask.is_quotable() && has_enough_quote_liquidity {
selected = Some((pool, bid_ask));
break;
}
}
let Some((metadata, bid_ask)) = selected else {
eprintln!("Metric live API returned no liquid Base WETH/USDC pool; skipping");
return;
};
let state = MetricState::new(weth, usdc, metadata, bid_ask, client);
assert!(state.bid_ask.is_quotable());
let amount_in = BigUint::from(1_000_000_000u64);
let indicative_quote = state
.get_amount_out(amount_in.clone(), &state.base_token, &state.quote_token)
.unwrap();
let trader = Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap();
let signed_quote = state
.request_signed_quote(GetAmountOutParams {
amount_in,
token_in: state.base_token.address.clone(),
token_out: state.quote_token.address.clone(),
sender: trader.clone(),
receiver: trader,
})
.await
.unwrap();
assert!(indicative_quote.amount > BigUint::from(0u8));
assert!(signed_quote.amount_out > BigUint::from(0u8));
assert_eq!(signed_quote.base_token, state.base_token.address);
assert_eq!(signed_quote.quote_token, state.quote_token.address);
assert!(signed_quote.quote_attributes.is_empty());
}
}