feat: add metric rfq integration#1000
Conversation
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.
Tip: disable this comment in your organization's Code Review settings.
| tokens: HashSet::new(), | ||
| tvl: 0.0, | ||
| quote_tokens: None, | ||
| base_url: "http://54.199.103.16:8080".to_string(), |
There was a problem hiding this comment.
Can we make this configurable somehow? If they change this URL - which is not unlikely, we'd need to release another version and users won't be able to fix until we act
There was a problem hiding this comment.
This is configurable now: users can override it with .base_url(...) function, and I also added env config for the default URL.
| let (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()?) |
There was a problem hiding this comment.
There is no price impact depending on the amount_in?
There was a problem hiding this comment.
I went back to the docs and realized I had missed the depth field from the /bid_ask endpoint.
Metric exposes orderbook-like depth bins on both the bid and ask sides. The volume is cumulative across bins, and within each bin the price follows a linear segment instead of staying flat.
I updated the implementation to handle those bins properly, and added comments with the formulas used for calculations.
You can also refer to the Metric docs for the depth description [here].(https://oracle-based-omm.gitbook.io/metric/RSm94m71kqtGICv4iKRj/developers/api#depth)
| let zero_for_one = zero_for_one(swap)?; | ||
| let price_limit_x64 = | ||
| if zero_for_one { Bytes::zero(32) } else { u256_bytes(&BigUint::from(u128::MAX)) }; | ||
| let oracle_update = if self.request_oracle_update { |
There was a problem hiding this comment.
Does this mean that, on Mainnet, the user will always pay for its transaction + the Oracle Update TX? If that's the case, can't we make this configurable?
There was a problem hiding this comment.
Or, it would be nice to try to swap without the oracle update and, if the TX reverts because the oracle didn't update, send the oracle update. Not sure how much that would add in gas or if that's feasible
| .unwrap_or_default() | ||
| .into(), | ||
| ); | ||
|
|
There was a problem hiding this comment.
let entries: [(&str, Vec<u8>); 9] = [
("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()),
("block_ts", bid_ask.block_ts.to_string().into_bytes()),
("server_ts", bid_ask.server_ts.to_string().into_bytes()),
("quote_expiration", bid_ask.quote_expiration.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());
}
is easier to read IMO - what do u think?
| 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 http_client = Client::new(); |
There was a problem hiding this comment.
By recreating the client here every time, you need to always redo TLS handshake and that adds latency. If you create the client once and reuse it you will get a performance increase on the requests
There was a problem hiding this comment.
fixed, now the client is cached with OnceLock, so we create it once and reuse it across requests.
|
|
||
| #[async_trait] | ||
| impl RFQClient for MetricClient { | ||
| fn stream( |
There was a problem hiding this comment.
Shouldn't this be async fn?
There was a problem hiding this comment.
stream returns a BoxStream directly and does the async work inside the stream
| Ok(biguint_to_u256(&value)) | ||
| } | ||
|
|
||
| fn stable_decimals(address: &Bytes) -> Option<u8> { |
There was a problem hiding this comment.
Not a big fan of this - if the RFQ adds a new stable we'd need to release a new Tycho-Simulation version?
Can we use Tycho data here?
There was a problem hiding this comment.
yes, agreed. I changed this so we can pass token metadata in from tycho data via .token_metadata(...).
| tokens: HashSet::new(), | ||
| tvl: 0.0, | ||
| quote_tokens: None, | ||
| base_url: "http://54.199.103.16:8080".to_string(), |
There was a problem hiding this comment.
Can we make this configurable somehow? If they change this URL - which is not unlikely, we'd need to release another version and users won't be able to fix until we act
| let res = GetAmountOutResult { | ||
| amount: capped_amount.clone(), | ||
| gas: BigUint::from(170_000u64), | ||
| new_state: self.clone_box(), |
There was a problem hiding this comment.
Shouldn't we calculate the depth impact of this swap and update the new_state with it?
There was a problem hiding this comment.
Hum, I’m not sure how a Metric swap should update the state, and the other RFQ sims also leave new_state unchanged.
| fn encoder(oracle_address: &Bytes) -> MetricSwapEncoder { | ||
| MetricSwapEncoder::new( | ||
| Bytes::zero(20), | ||
| Chain::Ethereum, |
There was a problem hiding this comment.
Should chain be hardcoded here?
There was a problem hiding this comment.
this is hardcoded for testing, and the test tokens are also on mainnet, so I think it should be fine.
| }; | ||
| let candidate = if &pool.token0 == token && quote_tokens.contains(&pool.token1) { | ||
| let Some(quote_token) = token_metadata.get(&pool.token1) else { | ||
| continue; |
There was a problem hiding this comment.
Aren't these else cases an error? Shouldn't we log something here?
There was a problem hiding this comment.
fixed, I added some logs when there's no candidate.
| #[serde(skip_serializing, default)] | ||
| secret_key: Option<String>, | ||
| #[serde(skip, default = "OnceLock::new")] | ||
| http_client: OnceLock<Client>, |
There was a problem hiding this comment.
just to reuse the same Client across requests, address this comment
There was a problem hiding this comment.
Couldn't we reuse it if it was just http_client: Client?
No description provided.