diff --git a/.github/workflows/starknet-js-test.yml b/.github/workflows/starknet-js-test.yml index 1feb0c4b3..8f2bc2898 100644 --- a/.github/workflows/starknet-js-test.yml +++ b/.github/workflows/starknet-js-test.yml @@ -26,7 +26,7 @@ jobs: fail-on-cache-miss: true - name: Setup dev chain and run tests run: | - ./target/release/madara --name madara --base-path ../madara_db --rpc-port 9944 --rpc-cors "*" --rpc-external --devnet --preset devnet --gas-price 0 --blob-gas-price 0 --no-l1-sync & + ./target/release/madara --name madara --base-path ../madara_db --rpc-port 9944 --rpc-cors "*" --rpc-external --devnet --preset devnet --gas-price 0 --blob-gas-price 0 --strk-gas-price 0 --strk-blob-gas-price 0 --no-l1-sync & MADARA_PID=$! while ! echo exit | nc localhost 9944; do sleep 1; done cd tests/js_tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b7908014..ae4714d6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Next release +- feat: fetch eth/strk price and sync strk gas price - feat(block_production): continue pending block on restart - feat(mempool): mempool transaction saving on db - feat(mempool): mempool transaction limits diff --git a/Cargo.lock b/Cargo.lock index bdb82c521..1cc47c31f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -332,7 +332,7 @@ dependencies = [ "async-stream", "async-trait", "auto_impl", - "dashmap 6.1.0", + "dashmap", "futures", "futures-utils-wasm", "lru", @@ -1232,6 +1232,19 @@ dependencies = [ "serde", ] +[[package]] +name = "bigdecimal" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d712318a27c7150326677b321a5fa91b55f6d9034ffd67f20319e147d40cee" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits 0.2.19", +] + [[package]] name = "bincode" version = "1.3.3" @@ -3587,19 +3600,6 @@ dependencies = [ "syn 2.0.89", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core 0.9.10", -] - [[package]] name = "dashmap" version = "6.1.0" @@ -4305,26 +4305,6 @@ dependencies = [ "minilp", ] -[[package]] -name = "governor" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" -dependencies = [ - "cfg-if", - "dashmap 5.5.3", - "futures", - "futures-timer", - "no-std-compat", - "nonzero_ext", - "parking_lot 0.12.3", - "portable-atomic", - "quanta", - "rand", - "smallvec", - "spinning_top", -] - [[package]] name = "group" version = "0.13.0" @@ -5388,7 +5368,6 @@ dependencies = [ "clap", "fdlimit", "futures", - "governor", "http 1.1.0", "hyper 0.14.31", "jsonrpsee", @@ -5406,6 +5385,7 @@ dependencies = [ "mc-telemetry", "mp-block", "mp-chain-config", + "mp-oracle", "mp-utils", "opentelemetry", "opentelemetry-appender-tracing", @@ -5422,7 +5402,6 @@ dependencies = [ "starknet_api", "thiserror 2.0.3", "tokio", - "tokio-util", "tower 0.4.13", "tower-http", "tracing", @@ -5668,8 +5647,8 @@ version = "0.7.0" dependencies = [ "alloy", "anyhow", + "bigdecimal", "bitvec", - "blockifier", "dotenv", "futures", "httpmock", @@ -5699,7 +5678,6 @@ dependencies = [ "thiserror 2.0.3", "time", "tokio", - "tokio-util", "tracing", "tracing-core", "tracing-opentelemetry", @@ -5762,7 +5740,6 @@ dependencies = [ "starknet-types-core 0.1.7 (git+https://github.com/kasarlabs/types-rs.git?branch=feat-deserialize-v0.1.7)", "starknet-types-rpc", "tokio", - "tokio-util", "tower 0.4.13", "tracing", "url", @@ -5813,6 +5790,7 @@ dependencies = [ "mp-chain-config", "mp-class", "mp-convert", + "mp-oracle", "mp-receipt", "mp-state-update", "mp-transactions", @@ -5825,7 +5803,9 @@ dependencies = [ "opentelemetry_sdk", "proptest", "proptest-derive", + "reqwest 0.12.8", "rstest 0.18.2", + "serde", "serde_json", "starknet-types-core 0.1.7 (git+https://github.com/kasarlabs/types-rs.git?branch=feat-deserialize-v0.1.7)", "starknet-types-rpc", @@ -5870,7 +5850,6 @@ dependencies = [ "starknet_api", "thiserror 2.0.3", "tokio", - "tokio-util", "tracing", ] @@ -5931,7 +5910,6 @@ dependencies = [ "serde_json", "sysinfo", "tokio", - "tokio-util", "tracing", ] @@ -6126,6 +6104,16 @@ dependencies = [ "url", ] +[[package]] +name = "mp-oracle" +version = "0.7.0" +dependencies = [ + "anyhow", + "async-trait", + "reqwest 0.12.8", + "serde", +] + [[package]] name = "mp-receipt" version = "0.7.0" @@ -6248,12 +6236,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - [[package]] name = "nom" version = "7.1.3" @@ -6264,12 +6246,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nonzero_ext" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" - [[package]] name = "ntapi" version = "0.4.1" @@ -6888,12 +6864,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "portable-atomic" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" - [[package]] name = "powerfmt" version = "0.2.0" @@ -7058,21 +7028,6 @@ dependencies = [ "syn 2.0.89", ] -[[package]] -name = "quanta" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi", - "web-sys", - "winapi", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -7134,15 +7089,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "raw-cpuid" -version = "11.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" -dependencies = [ - "bitflags 2.6.0", -] - [[package]] name = "rawpointer" version = "0.2.1" @@ -8237,15 +8183,6 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" -[[package]] -name = "spinning_top" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" -dependencies = [ - "lock_api", -] - [[package]] name = "spki" version = "0.7.3" diff --git a/Cargo.toml b/Cargo.toml index c6a1d0b2e..e46cfc2fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -116,6 +116,7 @@ mp-receipt = { path = "crates/primitives/receipt", default-features = false } mp-state-update = { path = "crates/primitives/state_update", default-features = false } mp-utils = { path = "crates/primitives/utils", default-features = false } mp-chain-config = { path = "crates/primitives/chain_config", default-features = false } +mp-oracle = { path = "crates/primitives/oracle", default-features = false } # Madara client mc-analytics = { path = "crates/client/analytics" } @@ -164,6 +165,7 @@ alloy = { version = "0.4.0", features = [ # Other third party dependencies paste = "1.0.15" anyhow = "1.0" +bigdecimal = "0.4.5" assert_matches = "1.5" async-trait = "0.1" base64 = "0.22" diff --git a/crates/client/eth/Cargo.toml b/crates/client/eth/Cargo.toml index 36f4c06b6..c424a75f1 100644 --- a/crates/client/eth/Cargo.toml +++ b/crates/client/eth/Cargo.toml @@ -19,24 +19,24 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] # Madara -mc-analytics = { workspace = true } -mc-db = { workspace = true } -mc-mempool = { workspace = true } -mp-chain-config = { workspace = true } -mp-convert = { workspace = true } -mp-transactions = { workspace = true } -mp-utils = { workspace = true } +mc-analytics.workspace = true +mc-db.workspace = true +mc-mempool.workspace = true +mp-chain-config.workspace = true +mp-convert.workspace = true +mp-transactions.workspace = true +mp-utils.workspace = true # Starknet -starknet-types-core = { workspace = true } -starknet_api = { workspace = true } +starknet-types-core.workspace = true +starknet_api.workspace = true # Other -alloy = { workspace = true } -anyhow = "1.0.75" -bitvec = { workspace = true } -blockifier = { workspace = true } +alloy.workspace = true +anyhow.workspace = true +bigdecimal.workspace = true +bitvec.workspace = true futures = { workspace = true, default-features = true } regex = "1.10.5" @@ -50,8 +50,7 @@ tokio = { workspace = true, features = [ "test-util", "signal", ] } -tokio-util = { workspace = true } -url = { workspace = true } +url.workspace = true #Instrumentation @@ -76,12 +75,12 @@ default = [] [dev-dependencies] -rstest = { workspace = true } -once_cell = { workspace = true } -tempfile = { workspace = true } -dotenv = { workspace = true } -httpmock = { workspace = true } +rstest.workspace = true +once_cell.workspace = true +tempfile.workspace = true +dotenv.workspace = true +httpmock.workspace = true tracing-test = "0.2.5" -serial_test = { workspace = true } -lazy_static = { workspace = true } +serial_test.workspace = true +lazy_static.workspace = true mp-utils = { workspace = true, features = ["testing"] } diff --git a/crates/client/eth/src/client.rs b/crates/client/eth/src/client.rs index 21c20a5b1..8d2e7659f 100644 --- a/crates/client/eth/src/client.rs +++ b/crates/client/eth/src/client.rs @@ -210,13 +210,23 @@ pub mod eth_client_getter_test { AnvilPortNum(guard.next.next().expect("no more port to use")) } + static ANVIL: Mutex>> = Mutex::new(None); + + pub fn get_shared_anvil() -> Arc { + let mut anvil = ANVIL.lock().expect("poisoned lock"); + if anvil.is_none() { + *anvil = Some(Arc::new(create_anvil_instance())); + } + Arc::clone(anvil.as_ref().unwrap()) + } + pub fn create_anvil_instance() -> AnvilInstance { let port = get_port(); let anvil = Anvil::new() .fork(FORK_URL.clone()) .fork_block_number(L1_BLOCK_NUMBER) .port(port.0) - .timeout(20_000) + .timeout(60_000) .try_spawn() .expect("failed to spawn anvil instance"); println!("Anvil started and running at `{}`", anvil.endpoint()); @@ -238,7 +248,7 @@ pub mod eth_client_getter_test { #[serial] #[tokio::test] async fn fail_create_new_client_invalid_core_contract() { - let anvil = create_anvil_instance(); + let anvil = get_shared_anvil(); // Sepolia core contract instead of mainnet const INVALID_CORE_CONTRACT_ADDRESS: &str = "0xE2Bb56ee936fd6433DC0F6e7e3b8365C906AA057"; @@ -254,7 +264,7 @@ pub mod eth_client_getter_test { #[serial] #[tokio::test] async fn get_latest_block_number_works() { - let anvil = create_anvil_instance(); + let anvil = get_shared_anvil(); let eth_client = create_ethereum_client(Some(anvil.endpoint().as_str())); let block_number = eth_client.provider.get_block_number().await.expect("issue while fetching the block number").as_u64(); @@ -264,7 +274,7 @@ pub mod eth_client_getter_test { #[serial] #[tokio::test] async fn get_last_event_block_number_works() { - let anvil = create_anvil_instance(); + let anvil = get_shared_anvil(); let eth_client = create_ethereum_client(Some(anvil.endpoint().as_str())); let block_number = eth_client .get_last_event_block_number::() @@ -276,7 +286,7 @@ pub mod eth_client_getter_test { #[serial] #[tokio::test] async fn get_last_verified_block_hash_works() { - let anvil = create_anvil_instance(); + let anvil = get_shared_anvil(); let eth_client = create_ethereum_client(Some(anvil.endpoint().as_str())); let block_hash = eth_client.get_last_verified_block_hash().await.expect("issue while getting the last verified block hash"); @@ -287,7 +297,7 @@ pub mod eth_client_getter_test { #[serial] #[tokio::test] async fn get_last_state_root_works() { - let anvil = create_anvil_instance(); + let anvil = get_shared_anvil(); let eth_client = create_ethereum_client(Some(anvil.endpoint().as_str())); let state_root = eth_client.get_last_state_root().await.expect("issue while getting the state root"); let expected = u256_to_felt(U256::from_str_radix(L2_STATE_ROOT, 10).unwrap()).unwrap(); @@ -297,7 +307,7 @@ pub mod eth_client_getter_test { #[serial] #[tokio::test] async fn get_last_verified_block_number_works() { - let anvil = create_anvil_instance(); + let anvil = get_shared_anvil(); let eth_client = create_ethereum_client(Some(anvil.endpoint().as_str())); let block_number = eth_client.get_last_verified_block_number().await.expect("issue"); assert_eq!(block_number, L2_BLOCK_NUMBER, "verified block number not matching"); diff --git a/crates/client/eth/src/l1_gas_price.rs b/crates/client/eth/src/l1_gas_price.rs index e6ca2b321..ad5986eec 100644 --- a/crates/client/eth/src/l1_gas_price.rs +++ b/crates/client/eth/src/l1_gas_price.rs @@ -2,6 +2,7 @@ use crate::client::EthereumClient; use alloy::eips::BlockNumberOrTag; use alloy::providers::Provider; use anyhow::Context; +use bigdecimal::BigDecimal; use mc_mempool::{GasPriceProvider, L1DataProvider}; use std::time::{Duration, UNIX_EPOCH}; @@ -68,6 +69,29 @@ async fn update_gas_price(eth_client: &EthereumClient, l1_gas_provider: GasPrice l1_gas_provider.update_eth_l1_gas_price(*eth_gas_price); l1_gas_provider.update_eth_l1_data_gas_price(avg_blob_base_fee); + // fetch eth/strk price and update + if let Some(oracle_provider) = &l1_gas_provider.oracle_provider { + let (eth_strk_price, decimals) = + oracle_provider.fetch_eth_strk_price().await.context("failed to retrieve ETH/STRK price")?; + let strk_gas_price = (BigDecimal::new((*eth_gas_price).into(), decimals.into()) + / BigDecimal::new(eth_strk_price.into(), decimals.into())) + .as_bigint_and_exponent(); + let strk_data_gas_price = (BigDecimal::new(avg_blob_base_fee.into(), decimals.into()) + / BigDecimal::new(eth_strk_price.into(), decimals.into())) + .as_bigint_and_exponent(); + + l1_gas_provider.update_strk_l1_gas_price( + strk_gas_price.0.to_str_radix(10).parse::().context("failed to update strk l1 gas price")?, + ); + l1_gas_provider.update_strk_l1_data_gas_price( + strk_data_gas_price + .0 + .to_str_radix(10) + .parse::() + .context("failed to update strk l1 data gas price")?, + ); + } + l1_gas_provider.update_last_update_timestamp(); // Update block number separately to avoid holding the lock for too long @@ -99,7 +123,7 @@ async fn update_l1_block_metrics(eth_client: &EthereumClient, l1_gas_provider: G #[cfg(test)] mod eth_client_gas_price_worker_test { use super::*; - use crate::client::eth_client_getter_test::{create_anvil_instance, create_ethereum_client}; + use crate::client::eth_client_getter_test::{create_ethereum_client, get_shared_anvil}; use httpmock::{MockServer, Regex}; use mc_mempool::GasPriceProvider; use serial_test::serial; @@ -110,7 +134,7 @@ mod eth_client_gas_price_worker_test { #[serial] #[tokio::test] async fn gas_price_worker_when_infinite_loop_true_works() { - let anvil = create_anvil_instance(); + let anvil = get_shared_anvil(); let eth_client = create_ethereum_client(Some(anvil.endpoint().as_str())); let l1_gas_provider = GasPriceProvider::new(); @@ -154,7 +178,7 @@ mod eth_client_gas_price_worker_test { #[serial] #[tokio::test] async fn gas_price_worker_when_infinite_loop_false_works() { - let anvil = create_anvil_instance(); + let anvil = get_shared_anvil(); let eth_client = create_ethereum_client(Some(anvil.endpoint().as_str())); let l1_gas_provider = GasPriceProvider::new(); @@ -173,7 +197,7 @@ mod eth_client_gas_price_worker_test { #[serial] #[tokio::test] async fn gas_price_worker_when_gas_price_fix_works() { - let anvil = create_anvil_instance(); + let anvil = get_shared_anvil(); let eth_client = create_ethereum_client(Some(anvil.endpoint().as_str())); let l1_gas_provider = GasPriceProvider::new(); l1_gas_provider.update_eth_l1_gas_price(20); @@ -194,7 +218,7 @@ mod eth_client_gas_price_worker_test { #[serial] #[tokio::test] async fn gas_price_worker_when_data_gas_price_fix_works() { - let anvil = create_anvil_instance(); + let anvil = get_shared_anvil(); let eth_client = create_ethereum_client(Some(anvil.endpoint().as_str())); let l1_gas_provider = GasPriceProvider::new(); l1_gas_provider.update_eth_l1_data_gas_price(20); @@ -279,7 +303,7 @@ mod eth_client_gas_price_worker_test { #[serial] #[tokio::test] async fn update_gas_price_works() { - let anvil = create_anvil_instance(); + let anvil = get_shared_anvil(); let eth_client = create_ethereum_client(Some(anvil.endpoint().as_str())); let l1_gas_provider = GasPriceProvider::new(); diff --git a/crates/client/gateway/client/Cargo.toml b/crates/client/gateway/client/Cargo.toml index 5f3ed5ab9..4576fc86a 100644 --- a/crates/client/gateway/client/Cargo.toml +++ b/crates/client/gateway/client/Cargo.toml @@ -37,7 +37,6 @@ hyper-tls.workspace = true hyper-util.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true -tokio-util.workspace = true tokio.workspace = true tower = { version = "0.4", features = ["timeout", "retry", "util", "limit"] } tracing.workspace = true diff --git a/crates/client/mempool/Cargo.toml b/crates/client/mempool/Cargo.toml index 9ce40fc20..aaad380af 100644 --- a/crates/client/mempool/Cargo.toml +++ b/crates/client/mempool/Cargo.toml @@ -44,6 +44,7 @@ mp-block.workspace = true mp-chain-config.workspace = true mp-class.workspace = true mp-convert.workspace = true +mp-oracle.workspace = true mp-receipt.workspace = true mp-state-update.workspace = true mp-transactions.workspace = true @@ -58,6 +59,8 @@ starknet_api.workspace = true # Other anyhow.workspace = true mockall = { workspace = true, optional = true } +reqwest.workspace = true +serde.workspace = true thiserror.workspace = true tokio-util.workspace = true tokio.workspace = true diff --git a/crates/client/mempool/src/l1.rs b/crates/client/mempool/src/l1.rs index b3bd98c6e..716cc9cc6 100644 --- a/crates/client/mempool/src/l1.rs +++ b/crates/client/mempool/src/l1.rs @@ -1,5 +1,6 @@ //! TODO: this should be in the backend use mp_block::header::{GasPrices, L1DataAvailabilityMode}; +use mp_oracle::Oracle; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::SystemTime; @@ -12,6 +13,7 @@ pub struct GasPriceProvider { data_gas_price_sync_enabled: Arc, strk_gas_price_sync_enabled: Arc, strk_data_gas_price_sync_enabled: Arc, + pub oracle_provider: Option>, } impl GasPriceProvider { @@ -23,9 +25,21 @@ impl GasPriceProvider { data_gas_price_sync_enabled: Arc::new(AtomicBool::new(true)), strk_gas_price_sync_enabled: Arc::new(AtomicBool::new(true)), strk_data_gas_price_sync_enabled: Arc::new(AtomicBool::new(true)), + oracle_provider: None, } } + pub fn is_oracle_needed(&self) -> bool { + self.gas_price_sync_enabled.load(Ordering::Relaxed) + && (self.strk_gas_price_sync_enabled.load(Ordering::Relaxed) + || self.strk_data_gas_price_sync_enabled.load(Ordering::Relaxed)) + } + + pub fn set_oracle_provider(&mut self, oracle_provider: impl Oracle + 'static) -> &mut Self { + self.oracle_provider = Some(Arc::new(oracle_provider)); + self + } + pub fn set_gas_prices(&self, new_prices: GasPrices) { self.update_eth_l1_gas_price(new_prices.eth_l1_gas_price); self.update_strk_l1_gas_price(new_prices.strk_l1_gas_price); @@ -42,11 +56,11 @@ impl GasPriceProvider { } pub fn set_strk_gas_price_sync_enabled(&self, enabled: bool) { - self.gas_price_sync_enabled.store(enabled, Ordering::Relaxed); + self.strk_gas_price_sync_enabled.store(enabled, Ordering::Relaxed); } pub fn set_strk_data_gas_price_sync_enabled(&self, enabled: bool) { - self.data_gas_price_sync_enabled.store(enabled, Ordering::Relaxed); + self.strk_data_gas_price_sync_enabled.store(enabled, Ordering::Relaxed); } pub fn update_last_update_timestamp(&self) { diff --git a/crates/client/rpc/Cargo.toml b/crates/client/rpc/Cargo.toml index 0d958bfff..45f1e4280 100644 --- a/crates/client/rpc/Cargo.toml +++ b/crates/client/rpc/Cargo.toml @@ -55,5 +55,4 @@ serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } -tokio-util = { workspace = true } tracing = { workspace = true } diff --git a/crates/client/telemetry/Cargo.toml b/crates/client/telemetry/Cargo.toml index 8e22dd603..938ac12af 100644 --- a/crates/client/telemetry/Cargo.toml +++ b/crates/client/telemetry/Cargo.toml @@ -26,5 +26,4 @@ reqwest-websocket = "0.3.0" serde_json = { workspace = true } sysinfo = "0.30.12" tokio = { workspace = true } -tokio-util = { workspace = true } tracing = { workspace = true } diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 70b9a3042..05240ceea 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -35,6 +35,7 @@ mc-sync = { workspace = true } mc-telemetry = { workspace = true } mp-block = { workspace = true } mp-chain-config = { workspace = true } +mp-oracle = { workspace = true } mp-utils = { workspace = true } # Starknet @@ -48,7 +49,6 @@ async-trait.workspace = true clap = { workspace = true, features = ["derive", "env"] } fdlimit.workspace = true futures = { workspace = true, features = ["thread-pool"] } -governor.workspace = true http.workspace = true hyper = { version = "0.14", features = ["server"] } jsonrpsee.workspace = true @@ -59,7 +59,6 @@ serde = { workspace = true, features = ["derive"] } serde_json.workspace = true serde_yaml.workspace = true thiserror.workspace = true -tokio-util.workspace = true tokio.workspace = true tower-http.workspace = true tower.workspace = true diff --git a/crates/node/src/cli/l1.rs b/crates/node/src/cli/l1.rs index 62607b2ab..cd5edf7c0 100644 --- a/crates/node/src/cli/l1.rs +++ b/crates/node/src/cli/l1.rs @@ -30,6 +30,14 @@ pub struct L1SyncParams { #[clap(env = "MADARA_STRK_DATA_GAS_PRICE", long, alias = "strk-blob-gas-price")] pub strk_blob_gas_price: Option, + /// Oracle API url. + #[clap(env = "ORACLE_URL", long, alias = "oracle-url")] + pub oracle_url: Option, + + /// Oracle API key. + #[clap(env = "ORACLE_API_KEY", long, alias = "oracle-api-key")] + pub oracle_api_key: Option, + /// Time in which the gas price worker will fetch the gas price. #[clap( env = "MADARA_GAS_PRICE_POLL", diff --git a/crates/node/src/main.rs b/crates/node/src/main.rs index 453ab5a23..f30f3e80c 100644 --- a/crates/node/src/main.rs +++ b/crates/node/src/main.rs @@ -5,7 +5,7 @@ mod cli; mod service; mod util; -use anyhow::Context; +use anyhow::{bail, Context}; use clap::Parser; use cli::{NetworkType, RunCmd}; use http::{HeaderName, HeaderValue}; @@ -16,6 +16,7 @@ use mc_gateway_client::GatewayProvider; use mc_mempool::{GasPriceProvider, L1DataProvider, Mempool, MempoolLimits}; use mc_rpc::providers::{AddTransactionProvider, ForwardToProvider, MempoolAddTxProvider}; use mc_telemetry::{SysInfo, TelemetryService}; +use mp_oracle::pragma::PragmaOracleBuilder; use mp_utils::service::{Service, ServiceGroup}; use service::{BlockProductionService, GatewayService, L1SyncService, L2SyncService, RpcService}; use std::sync::Arc; @@ -88,7 +89,7 @@ async fn main() -> anyhow::Result<()> { .context("Initializing importer service")?, ); - let l1_gas_setter = GasPriceProvider::new(); + let mut l1_gas_setter = GasPriceProvider::new(); if let Some(fix_gas) = run_cmd.l1_sync_params.gas_price { l1_gas_setter.update_eth_l1_gas_price(fix_gas as u128); @@ -106,6 +107,23 @@ async fn main() -> anyhow::Result<()> { l1_gas_setter.update_strk_l1_data_gas_price(strk_fix_blob_gas as u128); l1_gas_setter.set_strk_data_gas_price_sync_enabled(false); } + if let Some(ref oracle_url) = run_cmd.l1_sync_params.oracle_url { + if let Some(ref oracle_api_key) = run_cmd.l1_sync_params.oracle_api_key { + let oracle = PragmaOracleBuilder::new() + .with_api_url(oracle_url.clone()) + .with_api_key(oracle_api_key.clone()) + .build(); + l1_gas_setter.set_oracle_provider(oracle); + } + } + + if !run_cmd.l1_sync_params.sync_l1_disabled + && l1_gas_setter.is_oracle_needed() + && l1_gas_setter.oracle_provider.is_none() + { + bail!("STRK gas is not fixed and oracle is not provided"); + } + let l1_data_provider: Arc = Arc::new(l1_gas_setter.clone()); // declare mempool here so that it can be used to process l1->l2 messages in the l1 service diff --git a/crates/primitives/oracle/Cargo.toml b/crates/primitives/oracle/Cargo.toml new file mode 100644 index 000000000..898e3735e --- /dev/null +++ b/crates/primitives/oracle/Cargo.toml @@ -0,0 +1,23 @@ +[package] +description = "Madara primitive for Oracles" +name = "mp-oracle" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true +homepage.workspace = true + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] + +# Other +anyhow.workspace = true +async-trait.workspace = true +reqwest.workspace = true +serde = { workspace = true, features = ["derive"] } diff --git a/crates/primitives/oracle/src/lib.rs b/crates/primitives/oracle/src/lib.rs new file mode 100644 index 000000000..c3bd574de --- /dev/null +++ b/crates/primitives/oracle/src/lib.rs @@ -0,0 +1,8 @@ +use async_trait::async_trait; + +pub mod pragma; + +#[async_trait] +pub trait Oracle: Send + Sync { + async fn fetch_eth_strk_price(&self) -> anyhow::Result<(u128, u32)>; +} diff --git a/crates/primitives/oracle/src/pragma.rs b/crates/primitives/oracle/src/pragma.rs new file mode 100644 index 000000000..5a72b0c04 --- /dev/null +++ b/crates/primitives/oracle/src/pragma.rs @@ -0,0 +1,184 @@ +use std::fmt; + +use anyhow::{bail, Context}; +use async_trait::async_trait; +use reqwest::Url; +use serde::{Deserialize, Serialize}; + +use crate::Oracle; + +pub const DEFAULT_API_URL: &str = "https://api.dev.pragma.build/node/v1/data/"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PragmaOracle { + #[serde(default = "default_oracle_api_url")] + pub api_url: Url, + #[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) + } + + fn is_in_bounds(&self, price: u128) -> bool { + self.price_bounds.low <= price && price <= self.price_bounds.high + } +} + +#[async_trait] +impl Oracle for PragmaOracle { + /// Methods to retrieve ETH/STRK price from Pragma Oracle + /// + /// Return values: + /// Ok((u128, u32)) : return the price tuple as (price, decimals) + /// Err(e) : return an error if anything went wrong in the fetching process or eth/strk price is 0 + async fn fetch_eth_strk_price(&self) -> anyhow::Result<(u128, u32)> { + let response = reqwest::Client::new() + .get(self.get_fetch_url(String::from("eth"), String::from("strk"))) + .header("x-api-key", self.api_key.clone()) + .send() + .await + .context("failed to retrieve price from pragma oracle")?; + + let oracle_api_response = response.json::().await.context("failed to parse api response")?; + let eth_strk_price = u128::from_str_radix(oracle_api_response.price.trim_start_matches("0x"), 16) + .context("failed to parse price")?; + if eth_strk_price == 0 { + bail!("Pragma api returned 0 for eth/strk price"); + } + if !self.is_in_bounds(eth_strk_price) { + bail!("ETH/STRK price outside of bounds"); + } + Ok((eth_strk_price, oracle_api_response.decimals)) + } +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +/// Supported Aggregation Methods +#[serde(rename_all = "snake_case")] +pub enum AggregationMethod { + /// Computes the median value from the data. + Median, + /// Computes the mean (average) value from the data. + Mean, + /// Time Weighted Average Price. This is the default option. + #[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() -> Url { + // safe unwrap because its parsed from a const + Url::parse(DEFAULT_API_URL).unwrap() +} + +#[derive(Deserialize, Debug)] +struct PragmaApiResponse { + price: String, + decimals: u32, +} + +pub struct PragmaOracleBuilder { + api_url: Url, + api_key: String, +} + +impl Default for PragmaOracleBuilder { + fn default() -> Self { + Self { api_url: Url::parse("about:blank").expect("valid URL"), api_key: String::default() } + } +} + +impl PragmaOracleBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn with_api_url(mut self, api_url: Url) -> Self { + self.api_url = api_url; + self + } + + pub fn with_api_key(mut self, api_key: String) -> Self { + self.api_key = api_key; + self + } + + pub fn build(self) -> PragmaOracle { + PragmaOracle { + api_url: self.api_url, + api_key: self.api_key, + aggregation_method: AggregationMethod::default(), + interval: Interval::default(), + price_bounds: PriceBounds::default(), + } + } +}