diff --git a/crates/client/eth-client/src/config.rs b/crates/client/eth-client/src/config.rs index 3b2aabb1e..f52c4cad7 100644 --- a/crates/client/eth-client/src/config.rs +++ b/crates/client/eth-client/src/config.rs @@ -29,6 +29,8 @@ pub const DEFAULT_CHAIN_ID: u64 = 31337; /// PRE_PRIVATE=$(jq -r '.private_keys[0]' $BUILD_DIR/anvil.json) pub const DEFAULT_PRIVATE_KEY: &str = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; +pub const DEFAULT_API_URL: &str = "https://api.dev.pragma.build/node/v1/data/"; + #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct EthereumClientConfig { #[serde(default)] @@ -37,6 +39,8 @@ pub struct EthereumClientConfig { pub wallet: Option, #[serde(default)] pub contracts: StarknetContracts, + #[serde(default)] + pub oracle: OracleConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -70,6 +74,82 @@ pub struct HttpProviderConfig { pub gas_price_poll_ms: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +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, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PragmaOracle { + #[serde(default = "default_api_url")] + pub api_url: String, + #[serde(default)] + pub api_key: String, + #[serde(default)] + pub aggregation_method: AggregationMethod, + #[serde(default)] + pub interval: Interval, +} + +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, Copy)] +pub enum AggregationMethod { + #[serde(rename = "median")] + #[default] + Median, + #[serde(rename = "mean")] + Mean, + #[serde(rename = "twap")] + Twap, +} + +// impl fmt::Display for AggregationMethod { +// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +// match self { +// AggregationMethod::Median => write!(f, "median"), +// AggregationMethod::Mean => write!(f, "mean"), +// AggregationMethod::Twap => write!(f, "twap"), +// } +// } +// } + +// Supported Aggregation Intervals +#[derive(Default, Debug, Serialize, Deserialize, Clone, Copy)] +pub enum Interval { + #[serde(rename = "1min")] + #[default] + OneMinute, + #[serde(rename = "15min")] + FifteenMinutes, + #[serde(rename = "1h")] + OneHour, + #[serde(rename = "2h")] + TwoHours, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LocalWalletConfig { #[serde(default = "default_chain_id")] @@ -82,6 +162,10 @@ fn default_rpc_endpoint() -> String { DEFAULT_RPC_ENDPOINT.into() } +fn default_api_url() -> String { + DEFAULT_API_URL.into() +} + fn default_chain_id() -> u64 { DEFAULT_CHAIN_ID } @@ -90,6 +174,23 @@ fn default_private_key() -> String { DEFAULT_PRIVATE_KEY.to_string() } +impl Default for PragmaOracle { + fn default() -> Self { + Self { + api_url: default_api_url(), + api_key: String::from(""), + aggregation_method: AggregationMethod::Median, + interval: Interval::OneMinute, + } + } +} + +impl Default for OracleConfig { + fn default() -> Self { + Self::Pragma(PragmaOracle::default()) + } +} + impl Default for HttpProviderConfig { fn default() -> Self { Self { rpc_endpoint: default_rpc_endpoint(), tx_poll_interval_ms: None, gas_price_poll_ms: None } diff --git a/crates/client/l1-gas-price/src/worker.rs b/crates/client/l1-gas-price/src/worker.rs index 43b458cf1..8589e5e62 100644 --- a/crates/client/l1-gas-price/src/worker.rs +++ b/crates/client/l1-gas-price/src/worker.rs @@ -6,7 +6,7 @@ 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::config::{EthereumClientConfig, OracleConfig}; use mp_starknet_inherent::L1GasPrices; use serde::Deserialize; use tokio::time::sleep; @@ -27,7 +27,7 @@ pub async fn run_worker(config: Arc, gas_price: Arc log::trace!("Updated gas prices"), Err(e) => log::error!("Failed to update gas prices: {:?}", e), } @@ -60,6 +60,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()) @@ -96,37 +97,40 @@ async fn update_gas_price( 16, )?; - // TODO: fetch this from the oracle - let url = "https://api.dev.pragma.build/node/v1/data/eth/strk?interval=1min&aggregation=median"; - let api_key = ""; + 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 response = reqwest::Client::new().get(url).header("x-api-key", api_key).send().await?; + let res_json = response.json::().await; + + let mut gas_price = gas_price.lock().await; - let (price, decimal) = match response.json::().await { + match res_json { Ok(api_response) => { - log::trace!("Retrieved ETH/STRK price from Pragma API"); - (api_response.price, api_response.decimals) + log::trace!("Retrieved ETH/STRK price from Oracle"); + let eth_strk_price = u128::from_str_radix(api_response.price.trim_start_matches("0x"), 16)?; + 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"))?; } Err(e) => { log::error!("Failed to retrieve ETH/STRK price: {:?}", e); - (String::from(""), 0) } }; - let eth_strk_price = u128::from_str_radix(price.trim_start_matches("0x"), 16).expect("Invalid hex string"); - - let stark_gas = ((U256::from(eth_gas_price) * U256::from(eth_strk_price)) / 10u64.pow(decimal)).as_u128(); - let stark_data_gas = ((U256::from(avg_blob_base_fee) * U256::from(eth_strk_price)) / 10u64.pow(decimal)).as_u128(); - - let mut gas_price = gas_price.lock().await; 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(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"))?; + 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 b1b997e40..56b56fd80 100644 --- a/examples/messaging/eth-config.json +++ b/examples/messaging/eth-config.json @@ -5,5 +5,11 @@ }, "contracts": { "core_contract": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512" + }, + "oracle": { + "api_url": "https://api.dev.pragma.build/node/v1/data/eth/strk?interval=1min&aggregation=median", + "api_key": "", + "aggregation_method": "median", + "interval": "1min" } -} +} \ No newline at end of file