diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a03580f6c..354a65c71b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -348,3 +348,4 @@ - dev : clean contracts and compiled files - fix: add from_address in calldata of l1 message - test: add starkgate related testcase +- feat: add pragma api to compute fees diff --git a/crates/client/eth-client/src/config.rs b/crates/client/eth-client/src/config.rs index 3b2aabb1ef..4d0d0072d2 100644 --- a/crates/client/eth-client/src/config.rs +++ b/crates/client/eth-client/src/config.rs @@ -19,6 +19,7 @@ use ethers::types::{Address, H160}; use serde::{Deserialize, Serialize}; use crate::error::Error; +use crate::oracle::OracleConfig; /// Default Anvil local endpoint pub const DEFAULT_RPC_ENDPOINT: &str = "http://127.0.0.1:8545"; @@ -37,6 +38,8 @@ pub struct EthereumClientConfig { pub wallet: Option, #[serde(default)] pub contracts: StarknetContracts, + #[serde(default)] + pub oracle: OracleConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/client/eth-client/src/lib.rs b/crates/client/eth-client/src/lib.rs index 2b1a25ad86..3711ea6cf0 100644 --- a/crates/client/eth-client/src/lib.rs +++ b/crates/client/eth-client/src/lib.rs @@ -9,6 +9,7 @@ pub mod config; pub mod error; +pub mod oracle; use std::time::Duration; diff --git a/crates/client/eth-client/src/oracle.rs b/crates/client/eth-client/src/oracle.rs new file mode 100644 index 0000000000..50ca24b894 --- /dev/null +++ b/crates/client/eth-client/src/oracle.rs @@ -0,0 +1,134 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; + +pub const DEFAULT_API_URL: &str = "https://api.dev.pragma.build/node/v1/data/"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "oracle_name", content = "config")] +pub enum OracleConfig { + Pragma(PragmaOracle), +} + +impl OracleConfig { + pub fn get_fetch_url(&self, base: String, quote: String) -> String { + match self { + OracleConfig::Pragma(pragma_oracle) => pragma_oracle.get_fetch_url(base, quote), + } + } + + pub fn get_api_key(&self) -> &String { + match self { + OracleConfig::Pragma(oracle) => &oracle.api_key, + } + } + + pub fn is_in_bounds(&self, price: u128) -> bool { + match self { + OracleConfig::Pragma(oracle) => oracle.price_bounds.low <= price && price <= oracle.price_bounds.high, + } + } +} + +impl Default for OracleConfig { + fn default() -> Self { + Self::Pragma(PragmaOracle::default()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PragmaOracle { + #[serde(default = "default_oracle_api_url")] + pub api_url: String, + #[serde(default)] + pub api_key: String, + #[serde(default)] + pub aggregation_method: AggregationMethod, + #[serde(default)] + pub interval: Interval, + #[serde(default)] + pub price_bounds: PriceBounds, +} + +impl Default for PragmaOracle { + fn default() -> Self { + Self { + api_url: default_oracle_api_url(), + api_key: String::default(), + aggregation_method: AggregationMethod::Median, + interval: Interval::OneMinute, + price_bounds: Default::default(), + } + } +} + +impl PragmaOracle { + fn get_fetch_url(&self, base: String, quote: String) -> String { + format!("{}{}/{}?interval={}&aggregation={}", self.api_url, base, quote, self.interval, self.aggregation_method) + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +/// Supported Aggregation Methods +pub enum AggregationMethod { + #[serde(rename = "median")] + Median, + #[serde(rename = "mean")] + Mean, + #[serde(rename = "twap")] + #[default] + Twap, +} + +impl fmt::Display for AggregationMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + AggregationMethod::Median => "median", + AggregationMethod::Mean => "mean", + AggregationMethod::Twap => "twap", + }; + write!(f, "{}", name) + } +} + +/// Supported Aggregation Intervals +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +pub enum Interval { + #[serde(rename = "1min")] + OneMinute, + #[serde(rename = "15min")] + FifteenMinutes, + #[serde(rename = "1h")] + OneHour, + #[serde(rename = "2h")] + #[default] + TwoHours, +} + +impl fmt::Display for Interval { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + Interval::OneMinute => "1min", + Interval::FifteenMinutes => "15min", + Interval::OneHour => "1h", + Interval::TwoHours => "2h", + }; + write!(f, "{}", name) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PriceBounds { + pub low: u128, + pub high: u128, +} + +impl Default for PriceBounds { + fn default() -> Self { + Self { low: 0, high: u128::MAX } + } +} + +fn default_oracle_api_url() -> String { + DEFAULT_API_URL.into() +} diff --git a/crates/client/l1-gas-price/src/worker.rs b/crates/client/l1-gas-price/src/worker.rs index c90a91f71e..f03bd70fb9 100644 --- a/crates/client/l1-gas-price/src/worker.rs +++ b/crates/client/l1-gas-price/src/worker.rs @@ -3,23 +3,32 @@ use std::sync::Arc; use std::time::Duration; use anyhow::{format_err, Result}; +use ethers::types::U256; use ethers::utils::__serde_json::json; use futures::lock::Mutex; use mc_eth_client::config::EthereumClientConfig; +use mc_eth_client::oracle::OracleConfig; use mp_starknet_inherent::L1GasPrices; +use serde::Deserialize; use tokio::time::sleep; use crate::types::{EthRpcResponse, FeeHistory}; const DEFAULT_GAS_PRICE_POLL_MS: u64 = 10_000; +#[derive(Deserialize, Debug)] +struct OracleApiResponse { + price: String, + decimals: u32, +} + pub async fn run_worker(config: Arc, gas_price: Arc>, infinite_loop: bool) { let rpc_endpoint = config.provider.rpc_endpoint().clone(); let client = reqwest::Client::new(); let poll_time = config.provider.gas_price_poll_ms().unwrap_or(DEFAULT_GAS_PRICE_POLL_MS); loop { - match update_gas_price(rpc_endpoint.clone(), &client, gas_price.clone()).await { + match update_gas_price(rpc_endpoint.clone(), &client, gas_price.clone(), config.oracle.clone()).await { Ok(_) => log::trace!("Updated gas prices"), Err(e) => log::error!("Failed to update gas prices: {:?}", e), } @@ -52,6 +61,7 @@ async fn update_gas_price( rpc_endpoint: String, client: &reqwest::Client, gas_price: Arc>, + oracle: OracleConfig, ) -> Result<()> { let fee_history: EthRpcResponse = client .post(rpc_endpoint.clone()) @@ -88,18 +98,45 @@ async fn update_gas_price( 16, )?; - // TODO: fetch this from the oracle - let eth_strk_price = 2425; + let response = reqwest::Client::new() + .get(oracle.get_fetch_url(String::from("eth"), String::from("strk"))) + .header("x-api-key", oracle.get_api_key()) + .send() + .await?; + + let oracle_api_response = response.json::().await; let mut gas_price = gas_price.lock().await; + + match oracle_api_response { + Ok(api_response) => { + log::trace!("Retrieved ETH/STRK price from Oracle"); + let eth_strk_price = u128::from_str_radix(api_response.price.trim_start_matches("0x"), 16)?; + if oracle.is_in_bounds(eth_strk_price) { + let stark_gas = ((U256::from(eth_gas_price) * U256::from(eth_strk_price)) + / 10u64.pow(api_response.decimals)) + .as_u128(); + let stark_data_gas = ((U256::from(avg_blob_base_fee) * U256::from(eth_strk_price)) + / 10u64.pow(api_response.decimals)) + .as_u128(); + gas_price.strk_l1_gas_price = NonZeroU128::new(stark_gas) + .ok_or(format_err!("Failed to convert `strk_l1_gas_price` to NonZeroU128"))?; + gas_price.strk_l1_data_gas_price = NonZeroU128::new(stark_data_gas) + .ok_or(format_err!("Failed to convert `strk_l1_data_gas_price` to NonZeroU128"))?; + } else { + log::error!("⚠️ Retrieved price is outside of bounds"); + } + } + Err(e) => { + log::error!("Failed to retrieve ETH/STRK price: {:?}", e); + } + }; + gas_price.eth_l1_gas_price = NonZeroU128::new(eth_gas_price).ok_or(format_err!("Failed to convert `eth_gas_price` to NonZeroU128"))?; gas_price.eth_l1_data_gas_price = NonZeroU128::new(avg_blob_base_fee) .ok_or(format_err!("Failed to convert `eth_l1_data_gas_price` to NonZeroU128"))?; - gas_price.strk_l1_gas_price = NonZeroU128::new(eth_gas_price.saturating_mul(eth_strk_price)) - .ok_or(format_err!("Failed to convert `strk_l1_gas_price` to NonZeroU128"))?; - gas_price.strk_l1_data_gas_price = NonZeroU128::new(avg_blob_base_fee.saturating_mul(eth_strk_price)) - .ok_or(format_err!("Failed to convert `strk_l1_data_gas_price` to NonZeroU128"))?; + gas_price.last_update_timestamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_millis(); // explicitly dropping gas price here to avoid long waits when fetching the value // on the inherent side which would increase block time diff --git a/examples/messaging/eth-config.json b/examples/messaging/eth-config.json index b1b997e40f..46bb180b47 100644 --- a/examples/messaging/eth-config.json +++ b/examples/messaging/eth-config.json @@ -1,9 +1,22 @@ { "provider": { - "rpc_endpoint": "http://127.0.0.1:8545", + "rpc_endpoint": "https://ethereum-rpc.publicnode.com", "gas_price_poll_ms": 10000 }, "contracts": { "core_contract": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512" + }, + "oracle": { + "oracle_name": "Pragma", + "config": { + "api_url": "https://api.dev.pragma.build/node/v1/data/", + "api_key": "", + "aggregation_method": "twap", + "interval": "2h", + "price_bounds": { + "low": 3000000000000000000000, + "high": 6000000000000000000000 + } + } } }