1use std::{
2 collections::{HashMap, HashSet},
3 str::FromStr,
4 sync::LazyLock,
5 time::SystemTime,
6};
7
8use alloy::{
9 primitives::{Address, Bytes as AlloyBytes, U256},
10 sol_types::SolValue,
11};
12use async_trait::async_trait;
13use futures::stream::BoxStream;
14use num_bigint::BigUint;
15use num_traits::ToPrimitive;
16use reqwest::Client;
17use tokio::time::{interval, timeout, Duration};
18use tracing::{error, info, warn};
19use tycho_common::{
20 models::{
21 protocol::{GetAmountOutParams, ProtocolComponent, ProtocolComponentState},
22 token::Token,
23 Chain,
24 },
25 simulation::indicatively_priced::SignedQuote,
26 Bytes,
27};
28
29use crate::{
30 evm::protocol::u256_num::biguint_to_u256,
31 rfq::{
32 client::RFQClient,
33 errors::RFQError,
34 models::TimestampHeader,
35 protocols::metric::models::{
36 MetricBidAskResponse, MetricMetadata, MetricOracleUpdatePolicy,
37 MetricSignedOracleUpdateResponse, MetricSignedOracleUpdateSlot,
38 ORACLE_UPDATE_POLICY_ATTR,
39 },
40 },
41 tycho_client::feed::synchronizer::{ComponentWithState, Snapshot, StateSyncMessage},
42};
43
44static METRIC_HTTP_CLIENT: LazyLock<Client> = LazyLock::new(Client::new);
45
46#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
47pub struct MetricClient {
48 chain: Chain,
49 metadata_endpoint: String,
50 chain_endpoint: String,
52 tokens: HashSet<Bytes>,
53 #[serde(default)]
54 token_metadata: HashMap<Bytes, Token>,
55 tvl: f64,
56 quote_tokens: HashSet<Bytes>,
57 #[serde(skip_serializing, default)]
58 secret_key: Option<String>,
59 poll_time: Duration,
60 quote_timeout: Duration,
61 oracle_update_policy: MetricOracleUpdatePolicy,
62}
63
64impl MetricClient {
65 pub const PROTOCOL_SYSTEM: &'static str = "rfq:metric";
66
67 #[allow(clippy::too_many_arguments)]
68 pub fn new(
69 chain: Chain,
70 tokens: HashSet<Bytes>,
71 tvl: f64,
72 quote_tokens: HashSet<Bytes>,
73 base_url: String,
74 secret_key: Option<String>,
75 poll_time: Duration,
76 quote_timeout: Duration,
77 ) -> Result<Self, RFQError> {
78 Self::new_with_token_metadata(
79 chain,
80 tokens,
81 HashMap::new(),
82 tvl,
83 quote_tokens,
84 base_url,
85 secret_key,
86 poll_time,
87 quote_timeout,
88 MetricOracleUpdatePolicy::default_for_chain(chain),
89 )
90 }
91
92 #[allow(clippy::too_many_arguments)]
93 pub(super) fn new_with_token_metadata(
94 chain: Chain,
95 tokens: HashSet<Bytes>,
96 token_metadata: HashMap<Bytes, Token>,
97 tvl: f64,
98 quote_tokens: HashSet<Bytes>,
99 base_url: String,
100 secret_key: Option<String>,
101 poll_time: Duration,
102 quote_timeout: Duration,
103 oracle_update_policy: MetricOracleUpdatePolicy,
104 ) -> Result<Self, RFQError> {
105 let chain_path = chain_to_metric_path(chain)?;
106 let base_url = base_url.trim_end_matches('/');
107 let chain_endpoint = format!("{base_url}/{chain_path}");
108 Ok(Self {
109 chain,
110 metadata_endpoint: format!("{chain_endpoint}/metadata"),
111 chain_endpoint,
112 tokens,
113 token_metadata,
114 tvl,
115 quote_tokens,
116 secret_key,
117 poll_time,
118 quote_timeout,
119 oracle_update_policy,
120 })
121 }
122
123 fn http_client(&self) -> &Client {
124 &METRIC_HTTP_CLIENT
125 }
126
127 pub fn create_component_with_state(
128 &self,
129 component_id: String,
130 metadata: &MetricMetadata,
131 bid_ask: &MetricBidAskResponse,
132 tvl: f64,
133 ) -> ComponentWithState {
134 let mut static_attributes = HashMap::new();
135 static_attributes.insert(
136 ORACLE_UPDATE_POLICY_ATTR.to_string(),
137 self.oracle_update_policy
138 .as_attribute_value(),
139 );
140
141 let protocol_component = ProtocolComponent {
142 id: component_id.clone(),
143 protocol_system: Self::PROTOCOL_SYSTEM.to_string(),
144 protocol_type_name: "metric_pool".to_string(),
145 chain: self.chain,
146 tokens: vec![metadata.token0.clone(), metadata.token1.clone()],
147 contract_addresses: Vec::new(),
148 static_attributes,
149 ..Default::default()
150 };
151
152 let mut attributes = HashMap::new();
153
154 let entries: [(&str, Vec<u8>); 6] = [
155 ("bid_adj", bid_ask.bid_adj.as_bytes().to_vec()),
156 ("ask_adj", bid_ask.ask_adj.as_bytes().to_vec()),
157 (
158 "total_token0_available",
159 bid_ask
160 .total_token0_available
161 .as_bytes()
162 .to_vec(),
163 ),
164 (
165 "total_token1_available",
166 bid_ask
167 .total_token1_available
168 .as_bytes()
169 .to_vec(),
170 ),
171 (
172 "latest_block",
173 bid_ask
174 .latest_block
175 .to_string()
176 .into_bytes(),
177 ),
178 ("depth", serde_json::to_vec(&bid_ask.depth).unwrap_or_default()),
179 ];
180
181 for (key, bytes) in entries {
182 attributes.insert(key.to_string(), bytes.into());
183 }
184
185 ComponentWithState {
186 state: ProtocolComponentState::new(&component_id, attributes, HashMap::new()),
187 component: protocol_component,
188 component_tvl: Some(tvl),
189 entrypoints: vec![],
190 }
191 }
192
193 fn normalize_tvl(
194 &self,
195 pool: &MetricMetadata,
196 bid_ask: &MetricBidAskResponse,
197 pool_quotes: &[(MetricMetadata, MetricBidAskResponse)],
198 ) -> Option<f64> {
199 let token1_amount = metric_available_human(
202 bid_ask.total_token1_available(),
203 self.token_metadata
204 .get(&pool.token1)?
205 .decimals,
206 )?;
207
208 if self.quote_tokens.contains(&pool.token1) {
209 return Some(token1_amount);
210 }
211
212 let price = metric_price_in_quote_token(
215 &pool.token1,
216 pool_quotes,
217 &self.quote_tokens,
218 &self.token_metadata,
219 );
220 if price.is_none() {
221 warn!(
222 pool = ?pool.pool_address,
223 token1 = ?pool.token1,
224 "Metric one-hop TVL price not found for non-quote token"
225 );
226 }
227
228 price
229 .map(|price| token1_amount * price)
230 .filter(|tvl| tvl.is_finite() && *tvl >= 0.0)
231 }
232
233 async fn fetch_metadata(&self) -> Result<Vec<MetricMetadata>, RFQError> {
234 let response = self
235 .http_client()
236 .get(&self.metadata_endpoint)
237 .header("accept", "application/json")
238 .send()
239 .await
240 .map_err(|e| {
241 RFQError::ConnectionError(format!("Failed to fetch Metric metadata: {e}"))
242 })?;
243
244 if !response.status().is_success() {
245 return Err(RFQError::ConnectionError(format!(
246 "Metric metadata HTTP error {}: {}",
247 response.status(),
248 response
249 .text()
250 .await
251 .unwrap_or_default()
252 )));
253 }
254
255 response.json().await.map_err(|e| {
256 RFQError::ParsingError(format!("Failed to parse Metric metadata response: {e}"))
257 })
258 }
259
260 async fn fetch_bid_ask(&self, pool: &Bytes) -> Result<MetricBidAskResponse, RFQError> {
261 let endpoint =
262 format!("{}/{}/bid_ask", self.chain_endpoint, bytes_to_address_string(pool)?);
263 let mut request = self
264 .http_client()
265 .get(endpoint)
266 .header("accept", "application/json");
267
268 if let Some(secret_key) = &self.secret_key {
269 request = request.query(&[("secretKey", secret_key.as_str())]);
270 }
271
272 let response = request.send().await.map_err(|e| {
273 RFQError::ConnectionError(format!("Failed to fetch Metric bid/ask: {e}"))
274 })?;
275
276 if !response.status().is_success() {
277 return Err(RFQError::ConnectionError(format!(
278 "Metric bid/ask HTTP error {}: {}",
279 response.status(),
280 response
281 .text()
282 .await
283 .unwrap_or_default()
284 )));
285 }
286
287 response.json().await.map_err(|e| {
288 RFQError::ParsingError(format!("Failed to parse Metric bid/ask response: {e}"))
289 })
290 }
291
292 async fn fetch_signed_oracle_update(
293 &self,
294 pool: &Bytes,
295 ) -> Result<MetricSignedOracleUpdateResponse, RFQError> {
296 let endpoint =
297 format!("{}/{}/get_signed_data", self.chain_endpoint, bytes_to_address_string(pool)?);
298 let mut request = self
299 .http_client()
300 .get(endpoint)
301 .header("accept", "application/json");
302
303 if let Some(secret_key) = &self.secret_key {
304 request = request.query(&[("secretKey", secret_key.as_str())]);
305 }
306
307 let response = timeout(self.quote_timeout, request.send())
308 .await
309 .map_err(|_| {
310 RFQError::ConnectionError(format!(
311 "Metric oracle update request timed out after {} seconds",
312 self.quote_timeout.as_secs()
313 ))
314 })?
315 .map_err(|e| {
316 RFQError::ConnectionError(format!("Failed to fetch Metric oracle update: {e}"))
317 })?;
318
319 if !response.status().is_success() {
320 return Err(RFQError::QuoteNotFound(format!(
321 "Metric oracle update HTTP error {}: {}",
322 response.status(),
323 response
324 .text()
325 .await
326 .unwrap_or_default()
327 )));
328 }
329
330 response.json().await.map_err(|e| {
331 RFQError::ParsingError(format!("Failed to parse Metric oracle update response: {e}"))
332 })
333 }
334
335 pub async fn request_oracle_update_for_pool(
336 &self,
337 metadata: &MetricMetadata,
338 params: &GetAmountOutParams,
339 amount_out: BigUint,
340 ) -> Result<SignedQuote, RFQError> {
341 if !((params.token_in == metadata.token0 && params.token_out == metadata.token1) ||
342 (params.token_in == metadata.token1 && params.token_out == metadata.token0))
343 {
344 return Err(RFQError::InvalidInput(format!(
345 "Metric token pair mismatch: {} -> {} is not {} / {}",
346 params.token_in, params.token_out, metadata.token0, metadata.token1
347 )));
348 }
349
350 let oracle_update = self
351 .fetch_signed_oracle_update(&metadata.pool_address)
352 .await?;
353 let slot = oracle_update
356 .slots
357 .first()
358 .ok_or_else(|| {
359 RFQError::QuoteNotFound(format!(
360 "Metric oracle update returned no signed slots for pool {}",
361 metadata.pool_address
362 ))
363 })?;
364
365 let mut quote_attributes = HashMap::new();
366 quote_attributes.insert(
367 "oracle_update_0_args".to_string(),
368 encode_oracle_update_args(&oracle_update.feed_creator, slot)?,
369 );
370
371 Ok(SignedQuote {
372 base_token: params.token_in.clone(),
373 quote_token: params.token_out.clone(),
374 amount_in: params.amount_in.clone(),
375 amount_out,
376 quote_attributes,
377 })
378 }
379}
380
381#[async_trait]
382impl RFQClient for MetricClient {
383 fn stream(
384 &self,
385 ) -> BoxStream<'static, Result<(String, StateSyncMessage<TimestampHeader>), RFQError>> {
386 let client = self.clone();
387
388 Box::pin(async_stream::stream! {
389 let mut current_components: HashMap<String, ComponentWithState> = HashMap::new();
390 let mut ticker = interval(client.poll_time);
391
392 info!("Starting Metric polling every {} seconds", client.poll_time.as_secs());
393 loop {
394 ticker.tick().await;
395
396 let metadata = match client.fetch_metadata().await {
397 Ok(metadata) => metadata,
398 Err(e) => {
399 error!("Failed to fetch Metric metadata: {}", e);
400 continue;
401 }
402 };
403
404 let mut pool_quotes = Vec::new();
408 for pool in &metadata {
409 let bid_ask = match client.fetch_bid_ask(&pool.pool_address).await {
410 Ok(bid_ask) => bid_ask,
411 Err(e) => {
412 warn!(
413 "Failed to fetch Metric bid/ask for pool {}: {}",
414 pool.pool_address, e
415 );
416 continue;
417 }
418 };
419 if !bid_ask.quote_available {
420 continue;
421 }
422
423 pool_quotes.push((pool.clone(), bid_ask));
424 }
425
426 let mut new_components = HashMap::new();
427 for (pool, bid_ask) in &pool_quotes {
428 if !client.tokens.is_empty() &&
429 (!client.tokens.contains(&pool.token0) ||
430 !client.tokens.contains(&pool.token1))
431 {
432 continue;
433 }
434
435 let tvl = client
436 .normalize_tvl(pool, bid_ask, &pool_quotes)
437 .unwrap_or(0.0);
438 if tvl < client.tvl {
439 continue;
440 }
441
442 let component_id = pool.pool_address.to_string();
443 new_components.insert(
444 component_id.clone(),
445 client.create_component_with_state(component_id, pool, bid_ask, tvl),
446 );
447 }
448
449 let removed_components: HashMap<String, ProtocolComponent> = current_components
450 .iter()
451 .filter(|(id, _)| !new_components.contains_key(*id))
452 .map(|(id, component)| (id.clone(), component.component.clone()))
453 .collect();
454
455 current_components = new_components.clone();
456 let timestamp = SystemTime::now()
457 .duration_since(SystemTime::UNIX_EPOCH)
458 .map_err(|_| RFQError::ParsingError("SystemTime before UNIX EPOCH".to_string()))?
459 .as_secs();
460
461 yield Ok(("metric".to_string(), StateSyncMessage {
462 header: TimestampHeader { timestamp },
463 snapshots: Snapshot { states: new_components, vm_storage: HashMap::new() },
464 deltas: None,
465 removed_components,
466 }));
467 }
468 })
469 }
470
471 async fn request_binding_quote(
472 &self,
473 params: &GetAmountOutParams,
474 ) -> Result<SignedQuote, RFQError> {
475 let metadata = self.fetch_metadata().await?;
476 let pool = metadata
477 .iter()
478 .find(|pool| {
479 (params.token_in == pool.token0 && params.token_out == pool.token1) ||
480 (params.token_in == pool.token1 && params.token_out == pool.token0)
481 })
482 .ok_or_else(|| {
483 RFQError::QuoteNotFound(format!(
484 "Metric pool not found for {} -> {}",
485 params.token_in, params.token_out
486 ))
487 })?;
488
489 self.request_oracle_update_for_pool(pool, params, BigUint::default())
490 .await
491 }
492}
493
494fn chain_to_metric_path(chain: Chain) -> Result<&'static str, RFQError> {
495 match chain {
496 Chain::Ethereum => Ok("ethereum"),
497 Chain::Base => Ok("base"),
498 Chain::Bsc => Ok("bsc"),
499 Chain::Arbitrum => Ok("arbitrum"),
500 Chain::Polygon => Ok("polygon"),
501 unsupported => Err(RFQError::FatalError(format!(
502 "Metric does not support chain in this integration: {unsupported:?}"
503 ))),
504 }
505}
506
507fn bytes_to_address_string(address: &Bytes) -> Result<String, RFQError> {
508 if address.len() != 20 {
509 return Err(RFQError::InvalidInput(format!("Invalid EVM address length: {address}")));
510 }
511 Ok(Address::from_slice(address).to_checksum(None))
512}
513
514fn bytes_to_alloy_address(address: &Bytes) -> Result<Address, RFQError> {
515 if address.len() != 20 {
516 return Err(RFQError::InvalidInput(format!("Invalid EVM address length: {address}")));
517 }
518 Ok(Address::from_slice(address))
519}
520
521fn encode_oracle_update_args(
522 feed_creator: &Bytes,
523 slot: &MetricSignedOracleUpdateSlot,
524) -> Result<Bytes, RFQError> {
525 Ok((
526 bytes_to_alloy_address(feed_creator)?,
527 U256::from(slot.deadline),
528 biguint_decimal_to_u256(&slot.new_slot_value)?,
529 AlloyBytes::from(slot.signature.to_vec()),
530 )
531 .abi_encode()
532 .into())
533}
534
535fn biguint_decimal_to_u256(value: &str) -> Result<U256, RFQError> {
536 let value = BigUint::from_str(value)
537 .map_err(|_| RFQError::ParsingError(format!("Failed to parse uint value: {value}")))?;
538 Ok(biguint_to_u256(&value))
539}
540
541fn metric_price_in_quote_token(
542 token: &Bytes,
543 pool_quotes: &[(MetricMetadata, MetricBidAskResponse)],
544 quote_tokens: &HashSet<Bytes>,
545 token_metadata: &HashMap<Bytes, Token>,
546) -> Option<f64> {
547 let mut best: Option<(f64, f64)> = None;
548
549 for (pool, bid_ask) in pool_quotes {
550 let Some(mid_price) = metric_mid_price(bid_ask) else {
551 continue;
552 };
553 let candidate = if &pool.token0 == token && quote_tokens.contains(&pool.token1) {
554 let Some(quote_token) = token_metadata.get(&pool.token1) else {
555 continue;
556 };
557 let Some(quote_tvl) =
558 metric_available_human(bid_ask.total_token1_available(), quote_token.decimals)
559 else {
560 continue;
561 };
562 Some((mid_price, quote_tvl))
563 } else if &pool.token1 == token && quote_tokens.contains(&pool.token0) {
564 let Some(quote_token) = token_metadata.get(&pool.token0) else {
565 continue;
566 };
567 let Some(quote_tvl) =
568 metric_available_human(bid_ask.total_token0_available(), quote_token.decimals)
569 else {
570 continue;
571 };
572 Some((1.0 / mid_price, quote_tvl))
573 } else {
574 None
575 };
576
577 let Some((price, quote_tvl)) = candidate else {
578 continue;
579 };
580
581 if price.is_finite() &&
584 price > 0.0 &&
585 quote_tvl.is_finite() &&
586 quote_tvl > 0.0 &&
587 best.as_ref()
588 .is_none_or(|(_, best_quote_tvl)| quote_tvl > *best_quote_tvl)
589 {
590 best = Some((price, quote_tvl));
591 }
592 }
593
594 best.map(|(price, _)| price)
595}
596
597fn metric_mid_price(bid_ask: &MetricBidAskResponse) -> Option<f64> {
598 let bid = bid_ask.bid_price().ok()?;
599 let ask = bid_ask.ask_price().ok()?;
600 let mid = (bid + ask) / 2.0;
601 (mid.is_finite() && mid > 0.0).then_some(mid)
602}
603
604fn metric_available_human(amount: Result<BigUint, RFQError>, decimals: u32) -> Option<f64> {
605 amount
606 .ok()?
607 .to_f64()
608 .map(|raw| raw / 10_f64.powi(decimals as i32))
609 .filter(|amount| amount.is_finite() && *amount >= 0.0)
610}
611
612#[cfg(test)]
613mod tests {
614 use std::str::FromStr;
615
616 use tycho_common::models::token::Token;
617
618 use super::*;
619 use crate::rfq::protocols::metric::{
620 client_builder::MetricClientBuilder,
621 models::{MetricBidAskResponse, MetricDepth},
622 };
623
624 fn client() -> MetricClient {
625 MetricClient::new(
626 Chain::Ethereum,
627 HashSet::new(),
628 0.0,
629 HashSet::new(),
630 "http://localhost:8080".to_string(),
631 None,
632 Duration::from_secs(1),
633 Duration::from_secs(1),
634 )
635 .unwrap()
636 }
637
638 fn live_client() -> MetricClient {
639 let base_url = std::env::var("METRIC_API_URL")
640 .unwrap_or_else(|_| "http://54.199.103.16:8080".to_string());
641 MetricClient::new(
642 Chain::Ethereum,
643 HashSet::new(),
644 0.0,
645 HashSet::new(),
646 base_url,
647 None,
648 Duration::from_secs(1),
649 Duration::from_secs(5),
650 )
651 .unwrap()
652 }
653
654 fn client_with_tvl_config(
655 quote_tokens: HashSet<Bytes>,
656 token_metadata: HashMap<Bytes, Token>,
657 ) -> MetricClient {
658 MetricClient::new_with_token_metadata(
659 Chain::Ethereum,
660 HashSet::new(),
661 token_metadata,
662 0.0,
663 quote_tokens,
664 "http://localhost:8080".to_string(),
665 None,
666 Duration::from_secs(1),
667 Duration::from_secs(1),
668 MetricOracleUpdatePolicy::default_for_chain(Chain::Ethereum),
669 )
670 .unwrap()
671 }
672
673 fn metadata() -> MetricMetadata {
674 MetricMetadata {
675 pool_address: Bytes::from_str("0xbF48bCf474d57fF82A3215319229e0DE1476A557").unwrap(),
676 token0: Bytes::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(),
677 token1: Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
678 }
679 }
680
681 fn metric_address(seed: u8) -> Bytes {
682 Bytes::from(vec![seed; 20])
683 }
684
685 fn q64_price(price: u64) -> String {
686 (BigUint::from(price) << 64usize).to_string()
687 }
688
689 fn metadata_for_pool(token0: Bytes, token1: Bytes, seed: u8) -> MetricMetadata {
690 MetricMetadata { pool_address: metric_address(seed), token0, token1 }
691 }
692
693 fn bid_ask() -> MetricBidAskResponse {
694 MetricBidAskResponse {
695 bid_adj: "55340232221128654848000".to_string(),
696 ask_adj: "55358678965202364400000".to_string(),
697 quote_available: true,
698 total_token0_available: "1000000000000000000".to_string(),
699 total_token1_available: "3000000000".to_string(),
700 latest_block: 100,
701 depth: MetricDepth::default(),
702 }
703 }
704
705 fn bid_ask_with_prices(
706 bid: u64,
707 ask: u64,
708 total_token0_available: &str,
709 total_token1_available: &str,
710 ) -> MetricBidAskResponse {
711 MetricBidAskResponse {
712 bid_adj: q64_price(bid),
713 ask_adj: q64_price(ask),
714 quote_available: true,
715 total_token0_available: total_token0_available.to_string(),
716 total_token1_available: total_token1_available.to_string(),
717 latest_block: 100,
718 depth: MetricDepth::default(),
719 }
720 }
721
722 #[test]
723 fn test_builder_uses_token_metadata_decimals() {
724 let usdc = Bytes::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap();
725 let all_tokens = HashMap::from([(
726 usdc.clone(),
727 Token::new(&usdc, "USDC", 6, 0, &[], Chain::Ethereum, 100),
728 )]);
729
730 let client = MetricClientBuilder::new(Chain::Ethereum)
731 .token_metadata(all_tokens)
732 .build()
733 .unwrap();
734
735 assert_eq!(
736 client
737 .token_metadata
738 .get(&usdc)
739 .map(|token| token.decimals),
740 Some(6)
741 );
742 }
743
744 #[test]
745 fn test_normalize_tvl_uses_configured_quote_decimals() {
746 let pool = metadata();
747 let bid_ask = bid_ask();
748 let quote_tokens = HashSet::from([pool.token1.clone()]);
749 let token_metadata = HashMap::from([(
750 pool.token1.clone(),
751 Token::new(&pool.token1, "USDC", 6, 0, &[], Chain::Ethereum, 100),
752 )]);
753 let pool_quotes = vec![(pool.clone(), bid_ask.clone())];
754 let client = client_with_tvl_config(quote_tokens, token_metadata);
755
756 let tvl = client
757 .normalize_tvl(&pool, &bid_ask, &pool_quotes)
758 .unwrap();
759
760 assert_eq!(tvl, 3000.0);
761 }
762
763 #[test]
764 fn test_normalize_tvl_uses_best_one_hop_quote_pool() {
765 let reth = metric_address(10);
766 let weth = metric_address(20);
767 let usdc = metric_address(30);
768
769 let target = metadata_for_pool(reth, weth.clone(), 1);
770 let target_bid_ask =
771 bid_ask_with_prices(1, 1, "100000000000000000000", "2000000000000000000");
772 let low_liquidity_quote = metadata_for_pool(weth.clone(), usdc.clone(), 4);
773 let low_liquidity_bid_ask =
774 bid_ask_with_prices(3000, 3000, "1000000000000000000", "1000000000");
775 let high_liquidity_quote = metadata_for_pool(weth.clone(), usdc.clone(), 7);
776 let high_liquidity_bid_ask =
779 bid_ask_with_prices(3100, 3100, "1000000000000000000", "5000000000");
780 let pool_quotes = vec![
781 (target.clone(), target_bid_ask.clone()),
782 (low_liquidity_quote, low_liquidity_bid_ask),
783 (high_liquidity_quote, high_liquidity_bid_ask),
784 ];
785 let quote_tokens = HashSet::from([usdc.clone()]);
786 let token_metadata = HashMap::from([
787 (weth.clone(), Token::new(&weth, "WETH", 18, 0, &[], Chain::Ethereum, 100)),
788 (usdc.clone(), Token::new(&usdc, "USDC", 6, 0, &[], Chain::Ethereum, 100)),
789 ]);
790 let client = client_with_tvl_config(quote_tokens, token_metadata);
791
792 let tvl = client
793 .normalize_tvl(&target, &target_bid_ask, &pool_quotes)
794 .unwrap();
795
796 assert_eq!(tvl, 6200.0);
797 }
798
799 #[test]
800 fn test_component_attributes_round_trip_values() {
801 let metadata = metadata();
802 let component = client().create_component_with_state(
803 metadata.pool_address.to_string(),
804 &metadata,
805 &bid_ask(),
806 3000.0,
807 );
808
809 assert_eq!(component.component.protocol_system, MetricClient::PROTOCOL_SYSTEM);
810 assert_eq!(
811 component.component.tokens,
812 vec![metadata.token0.clone(), metadata.token1.clone()]
813 );
814 assert_eq!(
815 component.component.static_attributes[ORACLE_UPDATE_POLICY_ATTR],
816 MetricOracleUpdatePolicy::Always.as_attribute_value()
817 );
818 assert_eq!(component.component.id, metadata.pool_address.to_string());
819 assert!(component
820 .component
821 .contract_addresses
822 .is_empty());
823 assert_eq!(
824 String::from_utf8(component.state.attributes["bid_adj"].to_vec()).unwrap(),
825 "55340232221128654848000"
826 );
827 }
828
829 #[test]
830 fn test_builder_sets_oracle_update_policy_attribute() {
831 let client = MetricClientBuilder::new(Chain::Ethereum)
832 .oracle_update_policy(MetricOracleUpdatePolicy::RetryOnRevert)
833 .build()
834 .unwrap();
835
836 let component = client.create_component_with_state(
837 "metric_ethusdc".to_string(),
838 &metadata(),
839 &bid_ask(),
840 3000.0,
841 );
842
843 assert_eq!(
844 component.component.static_attributes[ORACLE_UPDATE_POLICY_ATTR],
845 MetricOracleUpdatePolicy::RetryOnRevert.as_attribute_value()
846 );
847 }
848
849 #[tokio::test]
850 #[ignore = "hits Metric's public API"]
851 async fn test_live_metric_api_fetch_bid_ask_latest_fields() {
852 let client = live_client();
853 let metadata = client.fetch_metadata().await.unwrap();
854 assert!(!metadata.is_empty());
855
856 let mut last_error = None;
857 let mut selected = None;
858 for pool in &metadata {
859 match client
860 .fetch_bid_ask(&pool.pool_address)
861 .await
862 {
863 Ok(bid_ask) => {
864 if bid_ask.quote_available &&
865 !bid_ask.depth.asks.is_empty() &&
866 !bid_ask.depth.bids.is_empty()
867 {
868 selected = Some((pool, bid_ask));
869 break;
870 }
871 }
872 Err(error) => last_error = Some(error.to_string()),
873 }
874 }
875
876 let Some((_pool, bid_ask)) = selected else {
877 panic!(
878 "Metric live API returned no quoteAvailable bid_ask response with ask and bid depth across {} pools; last error: {:?}",
879 metadata.len(),
880 last_error
881 );
882 };
883
884 let bid_price = bid_ask.bid_price().unwrap();
885 let ask_price = bid_ask.ask_price().unwrap();
886 assert!(bid_price.is_finite() && bid_price > 0.0);
887 assert!(ask_price.is_finite() && ask_price >= bid_price);
888 assert!(bid_ask.total_token0_available().is_ok());
889 assert!(bid_ask.total_token1_available().is_ok());
890 assert!(bid_ask.latest_block > 0);
891
892 for bin in bid_ask
893 .depth
894 .asks
895 .iter()
896 .chain(bid_ask.depth.bids.iter())
897 .take(6)
898 {
899 assert!(bin.price().unwrap().is_finite());
900 assert!(bin.cumulative_volume().is_ok());
901 }
902 }
903
904 #[test]
905 fn test_encode_oracle_update_args() {
906 let feed_creator = Bytes::from_str("0x0000000000000000000000000000000000000001").unwrap();
907 let slot = MetricSignedOracleUpdateSlot {
908 deadline: 1_700_000_000,
909 new_slot_value: "42".to_string(),
910 signature: Bytes::from_str(
911 "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111b",
912 )
913 .unwrap(),
914 };
915
916 let args = encode_oracle_update_args(&feed_creator, &slot).unwrap();
917
918 assert!(!args.starts_with(&[0x78, 0xce, 0x3a, 0xe1]));
919 assert!(!args.is_empty());
920 }
921}