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},
};
#[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("latest_block", &self.bid_ask.latest_block)
.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_human: f64,
token_out_decimals: u32,
max_output: &BigUint,
) -> Result<Option<DepthQuote>, SimulationError> {
let (start_price, bins) = match direction {
MetricDirection::ZeroForOne => (self.bid_ask.bid_price()?, &self.bid_ask.depth.bids),
MetricDirection::OneForZero => (self.bid_ask.ask_price()?, &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());
if effective_max_output.is_zero() {
return Ok(Some(DepthQuote {
amount_out_human: 0.0,
max_output: effective_max_output,
exhausted: amount_in_human > 0.0,
}));
}
let max_output_human =
raw_to_human(&effective_max_output, token_out_decimals, "depth max output")?;
let depth_fill = depth_output_for_input(
direction,
bins,
start_price,
amount_in_human,
token_out_decimals,
max_output_human,
)?;
Ok(Some(DepthQuote {
amount_out_human: depth_fill.output_human,
max_output: effective_max_output,
exhausted: depth_fill.exhausted,
}))
}
}
#[derive(Debug, Clone, Copy)]
enum MetricDirection {
ZeroForOne,
OneForZero,
}
struct DepthQuote {
amount_out_human: f64,
max_output: BigUint,
exhausted: bool,
}
struct DepthFill {
output_human: f64,
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 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, max_output) = match direction {
MetricDirection::ZeroForOne => {
let price = self.bid_ask.bid_price()?;
(amount_in_human * price, self.bid_ask.total_token1_available()?)
}
MetricDirection::OneForZero => {
let price = self.bid_ask.ask_price()?;
(amount_in_human / price, self.bid_ask.total_token0_available()?)
}
};
let depth_quote =
self.quote_with_depth(direction, amount_in_human, token_out.decimals, &max_output)?;
let (amount_out_human, effective_max_output, exhausted) = match depth_quote {
Some(quote) => (quote.amount_out_human, quote.max_output, quote.exhausted),
None => (flat_amount_out_human, max_output.clone(), false),
};
let amount_out =
BigUint::from_f64(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 capped_amount = if exhausted {
effective_max_output.clone()
} else {
amount_out
.clone()
.min(effective_max_output.clone())
};
let res = GetAmountOutResult {
amount: capped_amount.clone(),
gas: BigUint::from(170_000u64),
new_state: self.clone_box(),
};
if amount_out > effective_max_output {
return Err(SimulationError::InvalidInput(
format!(
"Metric pool has not enough liquidity. Requested output {amount_out} exceeds \
available {effective_max_output}"
),
Some(res),
));
}
if exhausted {
return Err(SimulationError::InvalidInput(
format!(
"Metric pool depth exhausted. Input {amount_in} cannot be fully filled; \
tradable depth caps output at {effective_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)?;
match direction {
MetricDirection::ZeroForOne => {
let price = self.bid_ask.bid_price()?;
let buy_limit =
cap_to_depth(self.bid_ask.total_token1_available()?, &self.bid_ask.depth.bids)?;
let buy_limit_human = buy_limit.to_f64().ok_or_else(|| {
SimulationError::RecoverableError("Can't convert buy limit to f64".into())
})? / 10_f64.powi(self.quote_token.decimals as i32);
let sell_limit = buy_limit_human / price;
let sell_limit =
BigUint::from_f64(sell_limit * 10_f64.powi(self.base_token.decimals as i32))
.ok_or_else(|| {
SimulationError::RecoverableError(
"Can't convert sell limit to BigUint".into(),
)
})?;
Ok((sell_limit, buy_limit))
}
MetricDirection::OneForZero => {
let price = self.bid_ask.ask_price()?;
let buy_limit =
cap_to_depth(self.bid_ask.total_token0_available()?, &self.bid_ask.depth.asks)?;
let buy_limit_human = buy_limit.to_f64().ok_or_else(|| {
SimulationError::RecoverableError("Can't convert buy limit to f64".into())
})? / 10_f64.powi(self.base_token.decimals as i32);
let sell_limit = buy_limit_human * price;
let sell_limit =
BigUint::from_f64(sell_limit * 10_f64.powi(self.quote_token.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]) -> Result<Option<BigUint>, SimulationError> {
bins.last()
.map(|bin| {
bin.cumulative_volume()
.map_err(SimulationError::from)
})
.transpose()
}
fn cap_to_depth(aggregate: BigUint, bins: &[MetricDepthBin]) -> Result<BigUint, SimulationError> {
match depth_max_output(bins)? {
Some(depth) => Ok(depth.min(aggregate)),
None => Ok(aggregate),
}
}
fn depth_output_for_input(
direction: MetricDirection,
bins: &[MetricDepthBin],
start_price: f64,
input_human: f64,
output_decimals: u32,
max_output_human: f64,
) -> Result<DepthFill, SimulationError> {
if input_human == 0.0 || max_output_human == 0.0 {
return Ok(DepthFill { output_human: 0.0, exhausted: input_human > 0.0 });
}
let mut current_price = start_price;
let mut previous_volume = 0.0;
let mut remaining_input = input_human;
let mut output_human = 0.0;
for bin in bins {
let bin_price = bin.price()?;
let cumulative =
raw_to_human(&bin.cumulative_volume()?, output_decimals, "depth cumulative volume")?;
let volume_in_bin = cumulative - previous_volume;
previous_volume = cumulative;
if volume_in_bin <= 0.0 {
current_price = bin_price;
continue;
}
let output_capacity = max_output_human - output_human;
if output_capacity <= 0.0 {
break;
}
let fillable_volume = volume_in_bin.min(output_capacity);
let segment_exit_price =
current_price + (bin_price - current_price) * fillable_volume / volume_in_bin;
let full_segment_input = depth_segment_input_required(
direction,
fillable_volume,
current_price,
segment_exit_price,
)?;
if remaining_input >= full_segment_input {
output_human += fillable_volume;
remaining_input -= full_segment_input;
current_price = segment_exit_price;
continue;
}
output_human += depth_segment_output_for_input(
direction,
remaining_input,
fillable_volume,
current_price,
segment_exit_price,
)?;
remaining_input = 0.0;
break;
}
if remaining_input > 1e-12 && output_human < max_output_human - 1e-12 {
return Err(SimulationError::RecoverableError(
"Metric depth has not enough cumulative volume".into(),
));
}
Ok(DepthFill {
output_human: output_human.min(max_output_human),
exhausted: remaining_input > 1e-12,
})
}
fn depth_segment_input_required(
direction: MetricDirection,
output_human: f64,
entry_price: f64,
exit_price: f64,
) -> Result<f64, SimulationError> {
let average_price = depth_segment_average_price(entry_price, exit_price)?;
match direction {
MetricDirection::ZeroForOne => Ok(output_human / average_price),
MetricDirection::OneForZero => Ok(output_human * average_price),
}
}
fn depth_segment_output_for_input(
direction: MetricDirection,
input_human: f64,
segment_output_human: f64,
entry_price: f64,
exit_price: f64,
) -> Result<f64, SimulationError> {
if segment_output_human <= 0.0 || entry_price <= 0.0 {
return Err(SimulationError::RecoverableError(
"Metric depth has invalid price curve".into(),
));
}
let average_slope = (exit_price - entry_price) / (2.0 * segment_output_human);
let output = match direction {
MetricDirection::ZeroForOne => {
let denominator = 1.0 - input_human * average_slope;
if denominator <= 0.0 {
return Err(SimulationError::RecoverableError(
"Metric depth has invalid price curve".into(),
));
}
input_human * entry_price / denominator
}
MetricDirection::OneForZero => {
if average_slope.abs() < 1e-18 {
input_human / entry_price
} else {
let discriminant =
entry_price.mul_add(entry_price, 4.0 * average_slope * input_human);
if discriminant < 0.0 {
return Err(SimulationError::RecoverableError(
"Metric depth has invalid price curve".into(),
));
}
let root_a = (-entry_price + discriminant.sqrt()) / (2.0 * average_slope);
let root_b = (-entry_price - discriminant.sqrt()) / (2.0 * average_slope);
[root_a, root_b]
.into_iter()
.find(|root| {
root.is_finite() && *root >= -1e-12 && *root <= segment_output_human + 1e-12
})
.ok_or_else(|| {
SimulationError::RecoverableError(
"Metric depth has invalid price curve".into(),
)
})?
}
}
};
Ok(output.clamp(0.0, segment_output_human))
}
fn depth_segment_average_price(entry_price: f64, exit_price: f64) -> Result<f64, SimulationError> {
let average_price = (entry_price + exit_price) / 2.0;
if average_price <= 0.0 {
return Err(SimulationError::RecoverableError(
"Metric depth has non-positive average price".into(),
));
}
Ok(average_price)
}
fn raw_to_human(amount: &BigUint, decimals: u32, field: &str) -> Result<f64, SimulationError> {
let amount = amount.to_f64().ok_or_else(|| {
SimulationError::RecoverableError(format!("Can't convert {field} to f64"))
})?;
Ok(amount / 10_f64.powi(decimals as i32))
}
#[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(self
.client
.request_oracle_update_for_pool(&self.metadata, ¶ms, amount_out)
.await?)
}
}
#[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 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 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(),
};
let bid_ask = MetricBidAskResponse {
bid_adj: "55340232221128654848000".to_string(),
ask_adj: "55524699661865750400000".to_string(),
quote_available: true,
total_token0_available: "10000000000000000000".to_string(),
total_token1_available: "30000000000".to_string(),
latest_block: 100,
depth: MetricDepth::default(),
};
let client = MetricClient::new(
Chain::Ethereum,
HashSet::new(),
0.0,
HashSet::new(),
"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 = "1500000000".to_string();
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: "53495557813757699686400".to_string(),
cumulative_volume: "3000000000".to_string(),
}];
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: "57184906628499610009600".to_string(),
cumulative_volume: cap.to_string(),
}];
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: "53495557813757699686400".to_string(),
cumulative_volume: "1500000000".to_string(),
}];
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(500_000_000_000_000_000u128));
}
#[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: "53495557813757699686400".to_string(),
cumulative_volume: "3000000000".to_string(),
}];
let result = state
.get_amount_out(
BigUint::from(1_000_000_000_000_000_000u128),
&state.base_token,
&state.quote_token,
)
.unwrap();
assert!(result.amount < BigUint::from(3_000_000_000u64));
assert!(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: "57184906628499610009600".to_string(),
cumulative_volume: "1000000000000000000".to_string(),
}];
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));
}
#[test]
fn test_depth_output_for_input_partially_fills_bid_bin() {
let bins = vec![MetricDepthBin {
bin_idx: 0,
price: "53495557813757699686400".to_string(),
cumulative_volume: "3000000000".to_string(),
}];
let fill =
depth_output_for_input(MetricDirection::ZeroForOne, &bins, 3000.0, 1.0, 6, 3000.0)
.unwrap();
assert!((fill.output_human - 2950.8196721311474).abs() < 1e-9);
assert!(!fill.exhausted);
}
#[test]
fn test_depth_output_for_input_partially_fills_ask_bin() {
let bins = vec![MetricDepthBin {
bin_idx: 0,
price: "57184906628499610009600".to_string(),
cumulative_volume: "1000000000000000000".to_string(),
}];
let fill =
depth_output_for_input(MetricDirection::OneForZero, &bins, 3010.0, 3000.0, 18, 1.0)
.unwrap();
assert!((fill.output_human - 0.9822534928279816).abs() < 1e-12);
assert!(!fill.exhausted);
}
#[test]
fn test_depth_output_for_input_exhausts_available_depth() {
let bins = vec![MetricDepthBin {
bin_idx: 0,
price: "53495557813757699686400".to_string(),
cumulative_volume: "3000000000".to_string(),
}];
let fill =
depth_output_for_input(MetricDirection::ZeroForOne, &bins, 3000.0, 2.0, 6, 3000.0)
.unwrap();
assert_eq!(fill.output_human, 3000.0);
assert!(fill.exhausted);
}
#[tokio::test]
#[ignore = "hits Metric's public API"]
async fn test_live_metric_api_state_get_amount_out_and_oracle_update() {
let weth = weth();
let usdc = usdc();
let base_url = std::env::var("METRIC_API_URL")
.unwrap_or_else(|_| "http://54.199.103.16:8080".to_string())
.trim_end_matches('/')
.to_string();
let client = MetricClient::new(
Chain::Ethereum,
HashSet::from([weth.address.clone(), usdc.address.clone()]),
0.0,
HashSet::new(),
base_url.clone(),
None,
Duration::from_secs(1),
Duration::from_secs(5),
)
.unwrap();
let http_client = reqwest::Client::new();
let metadata: Vec<MetricMetadata> = http_client
.get(format!("{base_url}/ethereum/metadata"))
.header("accept", "application/json")
.send()
.await
.unwrap()
.json()
.await
.unwrap();
let mut selected = None;
for pool in metadata
.into_iter()
.filter(|pool| pool.token0 == weth.address && pool.token1 == usdc.address)
{
let bid_ask: MetricBidAskResponse = http_client
.get(format!("{base_url}/ethereum/{}/bid_ask", pool.pool_address))
.header("accept", "application/json")
.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.quote_available && has_enough_quote_liquidity {
selected = Some((pool, bid_ask));
break;
}
}
let Some((metadata, bid_ask)) = selected else {
eprintln!("Metric live API returned no liquid Ethereum WETH/USDC pool; skipping");
return;
};
let state = MetricState::new(weth, usdc, metadata, bid_ask, client);
assert!(state.bid_ask.quote_available);
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["oracle_update_0_args"].is_empty());
}
}