use std::{
collections::{HashMap, HashSet},
str::FromStr,
sync::LazyLock,
time::SystemTime,
};
use alloy::{
primitives::{Address, Bytes as AlloyBytes, U256},
sol_types::SolValue,
};
use async_trait::async_trait;
use futures::stream::BoxStream;
use num_bigint::BigUint;
use num_traits::ToPrimitive;
use reqwest::Client;
use tokio::time::{interval, timeout, Duration};
use tracing::{error, info, warn};
use tycho_common::{
models::{
protocol::{GetAmountOutParams, ProtocolComponent, ProtocolComponentState},
token::Token,
Chain,
},
simulation::indicatively_priced::SignedQuote,
Bytes,
};
use crate::{
evm::protocol::u256_num::biguint_to_u256,
rfq::{
client::RFQClient,
errors::RFQError,
models::TimestampHeader,
protocols::metric::models::{
MetricBidAskResponse, MetricMetadata, MetricOracleUpdatePolicy,
MetricSignedOracleUpdateResponse, MetricSignedOracleUpdateSlot,
ORACLE_UPDATE_POLICY_ATTR,
},
},
tycho_client::feed::synchronizer::{ComponentWithState, Snapshot, StateSyncMessage},
};
static METRIC_HTTP_CLIENT: LazyLock<Client> = LazyLock::new(Client::new);
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct MetricClient {
chain: Chain,
metadata_endpoint: String,
chain_endpoint: String,
tokens: HashSet<Bytes>,
#[serde(default)]
token_metadata: HashMap<Bytes, Token>,
tvl: f64,
quote_tokens: HashSet<Bytes>,
#[serde(skip_serializing, default)]
secret_key: Option<String>,
poll_time: Duration,
quote_timeout: Duration,
oracle_update_policy: MetricOracleUpdatePolicy,
}
impl MetricClient {
pub const PROTOCOL_SYSTEM: &'static str = "rfq:metric";
#[allow(clippy::too_many_arguments)]
pub fn new(
chain: Chain,
tokens: HashSet<Bytes>,
tvl: f64,
quote_tokens: HashSet<Bytes>,
base_url: String,
secret_key: Option<String>,
poll_time: Duration,
quote_timeout: Duration,
) -> Result<Self, RFQError> {
Self::new_with_token_metadata(
chain,
tokens,
HashMap::new(),
tvl,
quote_tokens,
base_url,
secret_key,
poll_time,
quote_timeout,
MetricOracleUpdatePolicy::default_for_chain(chain),
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn new_with_token_metadata(
chain: Chain,
tokens: HashSet<Bytes>,
token_metadata: HashMap<Bytes, Token>,
tvl: f64,
quote_tokens: HashSet<Bytes>,
base_url: String,
secret_key: Option<String>,
poll_time: Duration,
quote_timeout: Duration,
oracle_update_policy: MetricOracleUpdatePolicy,
) -> Result<Self, RFQError> {
let chain_path = chain_to_metric_path(chain)?;
let base_url = base_url.trim_end_matches('/');
let chain_endpoint = format!("{base_url}/{chain_path}");
Ok(Self {
chain,
metadata_endpoint: format!("{chain_endpoint}/metadata"),
chain_endpoint,
tokens,
token_metadata,
tvl,
quote_tokens,
secret_key,
poll_time,
quote_timeout,
oracle_update_policy,
})
}
fn http_client(&self) -> &Client {
&METRIC_HTTP_CLIENT
}
pub fn create_component_with_state(
&self,
component_id: String,
metadata: &MetricMetadata,
bid_ask: &MetricBidAskResponse,
tvl: f64,
) -> ComponentWithState {
let mut static_attributes = HashMap::new();
static_attributes.insert(
ORACLE_UPDATE_POLICY_ATTR.to_string(),
self.oracle_update_policy
.as_attribute_value(),
);
let protocol_component = ProtocolComponent {
id: component_id.clone(),
protocol_system: Self::PROTOCOL_SYSTEM.to_string(),
protocol_type_name: "metric_pool".to_string(),
chain: self.chain,
tokens: vec![metadata.token0.clone(), metadata.token1.clone()],
contract_addresses: Vec::new(),
static_attributes,
..Default::default()
};
let mut attributes = HashMap::new();
let entries: [(&str, Vec<u8>); 6] = [
("bid_adj", bid_ask.bid_adj.as_bytes().to_vec()),
("ask_adj", bid_ask.ask_adj.as_bytes().to_vec()),
(
"total_token0_available",
bid_ask
.total_token0_available
.as_bytes()
.to_vec(),
),
(
"total_token1_available",
bid_ask
.total_token1_available
.as_bytes()
.to_vec(),
),
(
"latest_block",
bid_ask
.latest_block
.to_string()
.into_bytes(),
),
("depth", serde_json::to_vec(&bid_ask.depth).unwrap_or_default()),
];
for (key, bytes) in entries {
attributes.insert(key.to_string(), bytes.into());
}
ComponentWithState {
state: ProtocolComponentState::new(&component_id, attributes, HashMap::new()),
component: protocol_component,
component_tvl: Some(tvl),
entrypoints: vec![],
}
}
fn normalize_tvl(
&self,
pool: &MetricMetadata,
bid_ask: &MetricBidAskResponse,
pool_quotes: &[(MetricMetadata, MetricBidAskResponse)],
) -> Option<f64> {
let token1_amount = metric_available_human(
bid_ask.total_token1_available(),
self.token_metadata
.get(&pool.token1)?
.decimals,
)?;
if self.quote_tokens.contains(&pool.token1) {
return Some(token1_amount);
}
let price = metric_price_in_quote_token(
&pool.token1,
pool_quotes,
&self.quote_tokens,
&self.token_metadata,
);
if price.is_none() {
warn!(
pool = ?pool.pool_address,
token1 = ?pool.token1,
"Metric one-hop TVL price not found for non-quote token"
);
}
price
.map(|price| token1_amount * price)
.filter(|tvl| tvl.is_finite() && *tvl >= 0.0)
}
async fn fetch_metadata(&self) -> Result<Vec<MetricMetadata>, RFQError> {
let response = self
.http_client()
.get(&self.metadata_endpoint)
.header("accept", "application/json")
.send()
.await
.map_err(|e| {
RFQError::ConnectionError(format!("Failed to fetch Metric metadata: {e}"))
})?;
if !response.status().is_success() {
return Err(RFQError::ConnectionError(format!(
"Metric metadata HTTP error {}: {}",
response.status(),
response
.text()
.await
.unwrap_or_default()
)));
}
response.json().await.map_err(|e| {
RFQError::ParsingError(format!("Failed to parse Metric metadata response: {e}"))
})
}
async fn fetch_bid_ask(&self, pool: &Bytes) -> Result<MetricBidAskResponse, RFQError> {
let endpoint =
format!("{}/{}/bid_ask", self.chain_endpoint, bytes_to_address_string(pool)?);
let mut request = self
.http_client()
.get(endpoint)
.header("accept", "application/json");
if let Some(secret_key) = &self.secret_key {
request = request.query(&[("secretKey", secret_key.as_str())]);
}
let response = request.send().await.map_err(|e| {
RFQError::ConnectionError(format!("Failed to fetch Metric bid/ask: {e}"))
})?;
if !response.status().is_success() {
return Err(RFQError::ConnectionError(format!(
"Metric bid/ask HTTP error {}: {}",
response.status(),
response
.text()
.await
.unwrap_or_default()
)));
}
response.json().await.map_err(|e| {
RFQError::ParsingError(format!("Failed to parse Metric bid/ask response: {e}"))
})
}
async fn fetch_signed_oracle_update(
&self,
pool: &Bytes,
) -> Result<MetricSignedOracleUpdateResponse, RFQError> {
let endpoint =
format!("{}/{}/get_signed_data", self.chain_endpoint, bytes_to_address_string(pool)?);
let mut request = self
.http_client()
.get(endpoint)
.header("accept", "application/json");
if let Some(secret_key) = &self.secret_key {
request = request.query(&[("secretKey", secret_key.as_str())]);
}
let response = timeout(self.quote_timeout, request.send())
.await
.map_err(|_| {
RFQError::ConnectionError(format!(
"Metric oracle update request timed out after {} seconds",
self.quote_timeout.as_secs()
))
})?
.map_err(|e| {
RFQError::ConnectionError(format!("Failed to fetch Metric oracle update: {e}"))
})?;
if !response.status().is_success() {
return Err(RFQError::QuoteNotFound(format!(
"Metric oracle update HTTP error {}: {}",
response.status(),
response
.text()
.await
.unwrap_or_default()
)));
}
response.json().await.map_err(|e| {
RFQError::ParsingError(format!("Failed to parse Metric oracle update response: {e}"))
})
}
pub async fn request_oracle_update_for_pool(
&self,
metadata: &MetricMetadata,
params: &GetAmountOutParams,
amount_out: BigUint,
) -> Result<SignedQuote, RFQError> {
if !((params.token_in == metadata.token0 && params.token_out == metadata.token1) ||
(params.token_in == metadata.token1 && params.token_out == metadata.token0))
{
return Err(RFQError::InvalidInput(format!(
"Metric token pair mismatch: {} -> {} is not {} / {}",
params.token_in, params.token_out, metadata.token0, metadata.token1
)));
}
let oracle_update = self
.fetch_signed_oracle_update(&metadata.pool_address)
.await?;
let slot = oracle_update
.slots
.first()
.ok_or_else(|| {
RFQError::QuoteNotFound(format!(
"Metric oracle update returned no signed slots for pool {}",
metadata.pool_address
))
})?;
let mut quote_attributes = HashMap::new();
quote_attributes.insert(
"oracle_update_0_args".to_string(),
encode_oracle_update_args(&oracle_update.feed_creator, slot)?,
);
Ok(SignedQuote {
base_token: params.token_in.clone(),
quote_token: params.token_out.clone(),
amount_in: params.amount_in.clone(),
amount_out,
quote_attributes,
})
}
}
#[async_trait]
impl RFQClient for MetricClient {
fn stream(
&self,
) -> BoxStream<'static, Result<(String, StateSyncMessage<TimestampHeader>), RFQError>> {
let client = self.clone();
Box::pin(async_stream::stream! {
let mut current_components: HashMap<String, ComponentWithState> = HashMap::new();
let mut ticker = interval(client.poll_time);
info!("Starting Metric polling every {} seconds", client.poll_time.as_secs());
loop {
ticker.tick().await;
let metadata = match client.fetch_metadata().await {
Ok(metadata) => metadata,
Err(e) => {
error!("Failed to fetch Metric metadata: {}", e);
continue;
}
};
let mut pool_quotes = Vec::new();
for pool in &metadata {
let bid_ask = match client.fetch_bid_ask(&pool.pool_address).await {
Ok(bid_ask) => bid_ask,
Err(e) => {
warn!(
"Failed to fetch Metric bid/ask for pool {}: {}",
pool.pool_address, e
);
continue;
}
};
if !bid_ask.quote_available {
continue;
}
pool_quotes.push((pool.clone(), bid_ask));
}
let mut new_components = HashMap::new();
for (pool, bid_ask) in &pool_quotes {
if !client.tokens.is_empty() &&
(!client.tokens.contains(&pool.token0) ||
!client.tokens.contains(&pool.token1))
{
continue;
}
let tvl = client
.normalize_tvl(pool, bid_ask, &pool_quotes)
.unwrap_or(0.0);
if tvl < client.tvl {
continue;
}
let component_id = pool.pool_address.to_string();
new_components.insert(
component_id.clone(),
client.create_component_with_state(component_id, pool, bid_ask, tvl),
);
}
let removed_components: HashMap<String, ProtocolComponent> = current_components
.iter()
.filter(|(id, _)| !new_components.contains_key(*id))
.map(|(id, component)| (id.clone(), component.component.clone()))
.collect();
current_components = new_components.clone();
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|_| RFQError::ParsingError("SystemTime before UNIX EPOCH".to_string()))?
.as_secs();
yield Ok(("metric".to_string(), StateSyncMessage {
header: TimestampHeader { timestamp },
snapshots: Snapshot { states: new_components, vm_storage: HashMap::new() },
deltas: None,
removed_components,
}));
}
})
}
async fn request_binding_quote(
&self,
params: &GetAmountOutParams,
) -> Result<SignedQuote, RFQError> {
let metadata = self.fetch_metadata().await?;
let pool = metadata
.iter()
.find(|pool| {
(params.token_in == pool.token0 && params.token_out == pool.token1) ||
(params.token_in == pool.token1 && params.token_out == pool.token0)
})
.ok_or_else(|| {
RFQError::QuoteNotFound(format!(
"Metric pool not found for {} -> {}",
params.token_in, params.token_out
))
})?;
self.request_oracle_update_for_pool(pool, params, BigUint::default())
.await
}
}
fn chain_to_metric_path(chain: Chain) -> Result<&'static str, RFQError> {
match chain {
Chain::Ethereum => Ok("ethereum"),
Chain::Base => Ok("base"),
Chain::Bsc => Ok("bsc"),
Chain::Arbitrum => Ok("arbitrum"),
Chain::Polygon => Ok("polygon"),
unsupported => Err(RFQError::FatalError(format!(
"Metric does not support chain in this integration: {unsupported:?}"
))),
}
}
fn bytes_to_address_string(address: &Bytes) -> Result<String, RFQError> {
if address.len() != 20 {
return Err(RFQError::InvalidInput(format!("Invalid EVM address length: {address}")));
}
Ok(Address::from_slice(address).to_checksum(None))
}
fn bytes_to_alloy_address(address: &Bytes) -> Result<Address, RFQError> {
if address.len() != 20 {
return Err(RFQError::InvalidInput(format!("Invalid EVM address length: {address}")));
}
Ok(Address::from_slice(address))
}
fn encode_oracle_update_args(
feed_creator: &Bytes,
slot: &MetricSignedOracleUpdateSlot,
) -> Result<Bytes, RFQError> {
Ok((
bytes_to_alloy_address(feed_creator)?,
U256::from(slot.deadline),
biguint_decimal_to_u256(&slot.new_slot_value)?,
AlloyBytes::from(slot.signature.to_vec()),
)
.abi_encode_params()
.into())
}
fn biguint_decimal_to_u256(value: &str) -> Result<U256, RFQError> {
let value = BigUint::from_str(value)
.map_err(|_| RFQError::ParsingError(format!("Failed to parse uint value: {value}")))?;
Ok(biguint_to_u256(&value))
}
fn metric_price_in_quote_token(
token: &Bytes,
pool_quotes: &[(MetricMetadata, MetricBidAskResponse)],
quote_tokens: &HashSet<Bytes>,
token_metadata: &HashMap<Bytes, Token>,
) -> Option<f64> {
let mut best: Option<(f64, f64)> = None;
for (pool, bid_ask) in pool_quotes {
let Some(mid_price) = metric_mid_price(bid_ask) else {
continue;
};
let candidate = if &pool.token0 == token && quote_tokens.contains(&pool.token1) {
let Some(quote_token) = token_metadata.get(&pool.token1) else {
continue;
};
let Some(quote_tvl) =
metric_available_human(bid_ask.total_token1_available(), quote_token.decimals)
else {
continue;
};
Some((mid_price, quote_tvl))
} else if &pool.token1 == token && quote_tokens.contains(&pool.token0) {
let Some(quote_token) = token_metadata.get(&pool.token0) else {
continue;
};
let Some(quote_tvl) =
metric_available_human(bid_ask.total_token0_available(), quote_token.decimals)
else {
continue;
};
Some((1.0 / mid_price, quote_tvl))
} else {
None
};
let Some((price, quote_tvl)) = candidate else {
continue;
};
if price.is_finite() &&
price > 0.0 &&
quote_tvl.is_finite() &&
quote_tvl > 0.0 &&
best.as_ref()
.is_none_or(|(_, best_quote_tvl)| quote_tvl > *best_quote_tvl)
{
best = Some((price, quote_tvl));
}
}
best.map(|(price, _)| price)
}
fn metric_mid_price(bid_ask: &MetricBidAskResponse) -> Option<f64> {
let bid = bid_ask.bid_price().ok()?;
let ask = bid_ask.ask_price().ok()?;
let mid = (bid + ask) / 2.0;
(mid.is_finite() && mid > 0.0).then_some(mid)
}
fn metric_available_human(amount: Result<BigUint, RFQError>, decimals: u32) -> Option<f64> {
amount
.ok()?
.to_f64()
.map(|raw| raw / 10_f64.powi(decimals as i32))
.filter(|amount| amount.is_finite() && *amount >= 0.0)
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use tycho_common::models::token::Token;
use super::*;
use crate::rfq::protocols::metric::{
client_builder::MetricClientBuilder,
models::{MetricBidAskResponse, MetricDepth},
};
fn client() -> MetricClient {
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()
}
fn live_client() -> MetricClient {
let base_url = std::env::var("METRIC_API_URL")
.unwrap_or_else(|_| "http://54.199.103.16:8080".to_string());
MetricClient::new(
Chain::Ethereum,
HashSet::new(),
0.0,
HashSet::new(),
base_url,
None,
Duration::from_secs(1),
Duration::from_secs(5),
)
.unwrap()
}
fn client_with_tvl_config(
quote_tokens: HashSet<Bytes>,
token_metadata: HashMap<Bytes, Token>,
) -> MetricClient {
MetricClient::new_with_token_metadata(
Chain::Ethereum,
HashSet::new(),
token_metadata,
0.0,
quote_tokens,
"http://localhost:8080".to_string(),
None,
Duration::from_secs(1),
Duration::from_secs(1),
MetricOracleUpdatePolicy::default_for_chain(Chain::Ethereum),
)
.unwrap()
}
fn metadata() -> MetricMetadata {
MetricMetadata {
pool_address: Bytes::from_str("0xbF48bCf474d57fF82A3215319229e0DE1476A557").unwrap(),
token0: Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
token1: Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
}
}
fn metric_address(seed: u8) -> Bytes {
Bytes::from(vec![seed; 20])
}
fn q64_price(price: u64) -> String {
(BigUint::from(price) << 64usize).to_string()
}
fn metadata_for_pool(token0: Bytes, token1: Bytes, seed: u8) -> MetricMetadata {
MetricMetadata { pool_address: metric_address(seed), token0, token1 }
}
fn bid_ask() -> MetricBidAskResponse {
MetricBidAskResponse {
bid_adj: "55340232221128654848000".to_string(),
ask_adj: "55358678965202364400000".to_string(),
quote_available: true,
total_token0_available: "1000000000000000000".to_string(),
total_token1_available: "3000000000".to_string(),
latest_block: 100,
depth: MetricDepth::default(),
}
}
fn bid_ask_with_prices(
bid: u64,
ask: u64,
total_token0_available: &str,
total_token1_available: &str,
) -> MetricBidAskResponse {
MetricBidAskResponse {
bid_adj: q64_price(bid),
ask_adj: q64_price(ask),
quote_available: true,
total_token0_available: total_token0_available.to_string(),
total_token1_available: total_token1_available.to_string(),
latest_block: 100,
depth: MetricDepth::default(),
}
}
#[test]
fn test_builder_uses_token_metadata_decimals() {
let usdc = Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap();
let all_tokens = HashMap::from([(
usdc.clone(),
Token::new(&usdc, "USDC", 6, 0, &[], Chain::Ethereum, 100),
)]);
let client = MetricClientBuilder::new(Chain::Ethereum)
.token_metadata(all_tokens)
.build()
.unwrap();
assert_eq!(
client
.token_metadata
.get(&usdc)
.map(|token| token.decimals),
Some(6)
);
}
#[test]
fn test_normalize_tvl_uses_configured_quote_decimals() {
let pool = metadata();
let bid_ask = bid_ask();
let quote_tokens = HashSet::from([pool.token1.clone()]);
let token_metadata = HashMap::from([(
pool.token1.clone(),
Token::new(&pool.token1, "USDC", 6, 0, &[], Chain::Ethereum, 100),
)]);
let pool_quotes = vec![(pool.clone(), bid_ask.clone())];
let client = client_with_tvl_config(quote_tokens, token_metadata);
let tvl = client
.normalize_tvl(&pool, &bid_ask, &pool_quotes)
.unwrap();
assert_eq!(tvl, 3000.0);
}
#[test]
fn test_normalize_tvl_uses_best_one_hop_quote_pool() {
let reth = metric_address(10);
let weth = metric_address(20);
let usdc = metric_address(30);
let target = metadata_for_pool(reth, weth.clone(), 1);
let target_bid_ask =
bid_ask_with_prices(1, 1, "100000000000000000000", "2000000000000000000");
let low_liquidity_quote = metadata_for_pool(weth.clone(), usdc.clone(), 4);
let low_liquidity_bid_ask =
bid_ask_with_prices(3000, 3000, "1000000000000000000", "1000000000");
let high_liquidity_quote = metadata_for_pool(weth.clone(), usdc.clone(), 7);
let high_liquidity_bid_ask =
bid_ask_with_prices(3100, 3100, "1000000000000000000", "5000000000");
let pool_quotes = vec![
(target.clone(), target_bid_ask.clone()),
(low_liquidity_quote, low_liquidity_bid_ask),
(high_liquidity_quote, high_liquidity_bid_ask),
];
let quote_tokens = HashSet::from([usdc.clone()]);
let token_metadata = HashMap::from([
(weth.clone(), Token::new(&weth, "WETH", 18, 0, &[], Chain::Ethereum, 100)),
(usdc.clone(), Token::new(&usdc, "USDC", 6, 0, &[], Chain::Ethereum, 100)),
]);
let client = client_with_tvl_config(quote_tokens, token_metadata);
let tvl = client
.normalize_tvl(&target, &target_bid_ask, &pool_quotes)
.unwrap();
assert_eq!(tvl, 6200.0);
}
#[test]
fn test_component_attributes_round_trip_values() {
let metadata = metadata();
let component = client().create_component_with_state(
metadata.pool_address.to_string(),
&metadata,
&bid_ask(),
3000.0,
);
assert_eq!(component.component.protocol_system, MetricClient::PROTOCOL_SYSTEM);
assert_eq!(
component.component.tokens,
vec![metadata.token0.clone(), metadata.token1.clone()]
);
assert_eq!(
component.component.static_attributes[ORACLE_UPDATE_POLICY_ATTR],
MetricOracleUpdatePolicy::RetryOnRevert.as_attribute_value()
);
assert_eq!(component.component.id, metadata.pool_address.to_string());
assert!(component
.component
.contract_addresses
.is_empty());
assert_eq!(
String::from_utf8(component.state.attributes["bid_adj"].to_vec()).unwrap(),
"55340232221128654848000"
);
}
#[test]
fn test_builder_sets_oracle_update_policy_attribute() {
let client = MetricClientBuilder::new(Chain::Ethereum)
.oracle_update_policy(MetricOracleUpdatePolicy::RetryOnRevert)
.build()
.unwrap();
let component = client.create_component_with_state(
"metric_ethusdc".to_string(),
&metadata(),
&bid_ask(),
3000.0,
);
assert_eq!(
component.component.static_attributes[ORACLE_UPDATE_POLICY_ATTR],
MetricOracleUpdatePolicy::RetryOnRevert.as_attribute_value()
);
}
#[tokio::test]
#[ignore = "hits Metric's public API"]
async fn test_live_metric_api_fetch_bid_ask_latest_fields() {
let client = live_client();
let metadata = client.fetch_metadata().await.unwrap();
assert!(!metadata.is_empty());
let mut last_error = None;
let mut selected = None;
for pool in &metadata {
match client
.fetch_bid_ask(&pool.pool_address)
.await
{
Ok(bid_ask) => {
if bid_ask.quote_available &&
!bid_ask.depth.asks.is_empty() &&
!bid_ask.depth.bids.is_empty()
{
selected = Some((pool, bid_ask));
break;
}
}
Err(error) => last_error = Some(error.to_string()),
}
}
let Some((_pool, bid_ask)) = selected else {
panic!(
"Metric live API returned no quoteAvailable bid_ask response with ask and bid depth across {} pools; last error: {:?}",
metadata.len(),
last_error
);
};
let bid_price = bid_ask.bid_price().unwrap();
let ask_price = bid_ask.ask_price().unwrap();
assert!(bid_price.is_finite() && bid_price > 0.0);
assert!(ask_price.is_finite() && ask_price >= bid_price);
assert!(bid_ask.total_token0_available().is_ok());
assert!(bid_ask.total_token1_available().is_ok());
assert!(bid_ask.latest_block > 0);
for bin in bid_ask
.depth
.asks
.iter()
.chain(bid_ask.depth.bids.iter())
.take(6)
{
assert!(bin.price().unwrap().is_finite());
assert!(bin.cumulative_volume().is_ok());
}
}
#[test]
fn test_encode_oracle_update_args() {
let feed_creator = Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap();
let slot = MetricSignedOracleUpdateSlot {
deadline: 1_700_000_000,
new_slot_value: "42".to_string(),
signature: Bytes::from_str(
"0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111b",
)
.unwrap(),
};
let args = encode_oracle_update_args(&feed_creator, &slot).unwrap();
assert!(!args.starts_with(&[0x78, 0xce, 0x3a, 0xe1]));
assert!(!args.is_empty());
assert_eq!(args.len(), 256);
assert!(args[0..12].iter().all(|b| *b == 0));
assert_eq!(&args[12..32], feed_creator.as_ref());
}
}