From a4e6a286371feb034a588ed49d5ea0ad5c1223db Mon Sep 17 00:00:00 2001 From: tilacog Date: Wed, 24 Dec 2025 15:06:13 -0300 Subject: [PATCH 1/4] base test --- crates/e2e/tests/e2e/main.rs | 1 + crates/e2e/tests/e2e/native_price.rs | 93 ++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 crates/e2e/tests/e2e/native_price.rs diff --git a/crates/e2e/tests/e2e/main.rs b/crates/e2e/tests/e2e/main.rs index 02a8bd9d7f..1f2611a746 100644 --- a/crates/e2e/tests/e2e/main.rs +++ b/crates/e2e/tests/e2e/main.rs @@ -19,6 +19,7 @@ mod jit_orders; mod limit_orders; mod liquidity; mod liquidity_source_notification; +mod native_price; mod order_cancellation; mod partial_fill; mod partially_fillable_balance; diff --git a/crates/e2e/tests/e2e/native_price.rs b/crates/e2e/tests/e2e/native_price.rs new file mode 100644 index 0000000000..204cd2b687 --- /dev/null +++ b/crates/e2e/tests/e2e/native_price.rs @@ -0,0 +1,93 @@ +use { + e2e::setup::{OnchainComponents, Services, TIMEOUT, run_test, wait_for_condition}, + number::units::EthUnit, + shared::ethrpc::Web3, + std::sync::{Arc, Mutex}, +}; + +#[tokio::test] +#[ignore] +async fn local_node_native_price_forwarding() { + run_test(native_price_forwarding).await; +} + +/// Test that native price forwarding from orderbook to autopilot works +/// correctly. +/// +/// Architecture being tested: +/// User -> Orderbook (/api/v1/token/{token}/native_price) +/// -> Forwarder (configured in orderbook via `--native-price-estimators`) +/// -> Autopilot (/native_price/:token at port 12088) +/// -> Driver-based estimation +/// -> Returns price +/// +/// The forwarding chain is configured in `crates/e2e/src/setup/services.rs`: +/// - Orderbook uses `Forwarder|http://localhost:12088` (see `api_autopilot_arguments`) +/// - Autopilot uses `Driver|test_quoter|http://localhost:11088/test_solver` (see +/// `autopilot_arguments`) +async fn native_price_forwarding(web3: Web3) { + tracing::info!("Setting up chain state."); + let mut onchain = OnchainComponents::deploy(web3).await; + let [solver] = onchain.make_solvers(10u64.eth()).await; + + // Deploy token WITH UniV2 pool - this creates liquidity so price can be + // estimated + let [token] = onchain + .deploy_tokens_with_weth_uni_v2_pools(500u64.eth(), 1_000u64.eth()) + .await; + + tracing::info!("Starting services."); + let services = Services::new(&onchain).await; + services.start_protocol(solver).await; + + // Test 1: Token with liquidity returns a valid price via forwarding chain + tracing::info!("Testing native price for token with liquidity."); + let price_value = Arc::new(Mutex::new(-1.0)); + wait_for_condition(TIMEOUT, || async { + match services.get_native_price(token.address()).await { + Ok(p) => { + *price_value.lock().unwrap() = p.price; + true + } + _ => false, + } + }) + .await + .expect("Expected successful price for token with liquidity"); + + let price = *price_value.lock().unwrap(); + assert!( + price > 0.0, // TODO: can we use a "close enough" approximation here, like we do for the weth case below? Just comparing for greater than zero seems lazy + "Price should have been set to a positive value" + ); + tracing::info!(price, "Got native price for token"); + + // Test 2: WETH (native token) returns price of ~1.0 + tracing::info!("Testing native price for WETH."); + let weth_price_value = Arc::new(Mutex::new(-1.0)); + wait_for_condition(TIMEOUT, || async { + match services + .get_native_price(onchain.contracts().weth.address()) + .await + { + Ok(p) => { + *weth_price_value.lock().unwrap() = p.price; + true + } + _ => false, + } + }) + .await + .expect("Expected successful price for WETH"); + + let weth_price = *weth_price_value.lock().unwrap(); + assert!(weth_price >= 0.0, "WETH price should have been set"); + + // WETH price should be ~1.0, since it is the native token + assert!( + (weth_price - 1.0).abs() < 1e-6, + "WETH price should be ~1.0, got {}", + weth_price + ); + tracing::info!(weth_price, "Got native price for WETH"); +} From 98eb01334db9a30eef199f8861b18623dc80dc30 Mon Sep 17 00:00:00 2001 From: tilacog Date: Wed, 24 Dec 2025 15:06:32 -0300 Subject: [PATCH 2/4] test w/ shutting down autopilot --- crates/e2e/tests/e2e/native_price.rs | 76 ++++++++++++++++++++++++++-- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/crates/e2e/tests/e2e/native_price.rs b/crates/e2e/tests/e2e/native_price.rs index 204cd2b687..97a7c78ed2 100644 --- a/crates/e2e/tests/e2e/native_price.rs +++ b/crates/e2e/tests/e2e/native_price.rs @@ -1,8 +1,12 @@ use { - e2e::setup::{OnchainComponents, Services, TIMEOUT, run_test, wait_for_condition}, + autopilot::shutdown_controller::ShutdownController, + e2e::setup::{OnchainComponents, Services, TIMEOUT, colocation, run_test, wait_for_condition}, number::units::EthUnit, shared::ethrpc::Web3, - std::sync::{Arc, Mutex}, + std::{ + sync::{Arc, Mutex}, + time::Duration, + }, }; #[tokio::test] @@ -38,7 +42,49 @@ async fn native_price_forwarding(web3: Web3) { tracing::info!("Starting services."); let services = Services::new(&onchain).await; - services.start_protocol(solver).await; + + // Start services manually (instead of start_protocol) to get shutdown control + // over autopilot, allowing us to verify the forwarding dependency. + colocation::start_driver( + onchain.contracts(), + vec![ + colocation::start_baseline_solver( + "test_solver".into(), + solver.clone(), + *onchain.contracts().weth.address(), + vec![], + 1, + true, + ) + .await, + ], + colocation::LiquidityProvider::UniswapV2, + false, + ); + + let (shutdown_signal, shutdown_controller) = ShutdownController::new_manual_shutdown(); + let autopilot_handle = services + .start_autopilot_with_shutdown_controller( + None, + vec![ + format!( + "--drivers=test_solver|http://localhost:11088/test_solver|{}|requested-timeout-on-problems", + const_hex::encode(solver.address()) + ), + "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver" + .to_string(), + "--gas-estimators=http://localhost:11088/gasprice".to_string(), + ], + shutdown_controller, + ) + .await; + + services + .start_api(vec![ + "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver".to_string(), + "--gas-estimators=http://localhost:11088/gasprice".to_string(), + ]) + .await; // Test 1: Token with liquidity returns a valid price via forwarding chain tracing::info!("Testing native price for token with liquidity."); @@ -90,4 +136,28 @@ async fn native_price_forwarding(web3: Web3) { weth_price ); tracing::info!(weth_price, "Got native price for WETH"); + + // Test 3: Stop autopilot and verify price estimation fails (proves forwarding dependency) + tracing::info!("Stopping autopilot to verify forwarding dependency."); + shutdown_signal.shutdown(); + // Autopilot checks shutdown signal during its run loop, which is triggered by new blocks + onchain.mint_block().await; + tokio::time::timeout(Duration::from_secs(15), autopilot_handle) + .await + .expect("autopilot should shut down within timeout") + .expect("autopilot task should complete without panic"); + + // Wait for native price cache to expire (configured as 2s in services.rs) + tracing::info!("Waiting for native price cache to expire."); + tokio::time::sleep(Duration::from_secs(3)).await; + + tracing::info!("Verifying native price fails without autopilot."); + let result = services.get_native_price(token.address()).await; + assert!( + result.is_err(), + "Expected price request to fail after autopilot shutdown, proving the forwarding \ + dependency. Got: {:?}", + result + ); + tracing::info!("Confirmed: orderbook forwards native price requests to autopilot"); } From dc02a211fded84b178e30de9c0cd5616e9913db7 Mon Sep 17 00:00:00 2001 From: tilacog Date: Tue, 30 Dec 2025 10:49:03 -0300 Subject: [PATCH 3/4] refactor native_price e2e test --- crates/e2e/tests/e2e/native_price.rs | 228 +++++++++++++++------------ 1 file changed, 126 insertions(+), 102 deletions(-) diff --git a/crates/e2e/tests/e2e/native_price.rs b/crates/e2e/tests/e2e/native_price.rs index 97a7c78ed2..8d4645fa41 100644 --- a/crates/e2e/tests/e2e/native_price.rs +++ b/crates/e2e/tests/e2e/native_price.rs @@ -1,14 +1,120 @@ use { - autopilot::shutdown_controller::ShutdownController, - e2e::setup::{OnchainComponents, Services, TIMEOUT, colocation, run_test, wait_for_condition}, + alloy::primitives::Address, + autopilot::shutdown_controller::{ShutdownController, ShutdownSignal}, + e2e::setup::{ + OnchainComponents, + Services, + TIMEOUT, + TestAccount, + colocation, + run_test, + wait_for_condition, + }, number::units::EthUnit, shared::ethrpc::Web3, - std::{ - sync::{Arc, Mutex}, - time::Duration, - }, + std::{sync::Arc, time::Duration}, + tokio::{sync::Mutex, task::JoinHandle}, }; +/// Test environment with shutdown control over autopilot. +struct TestEnv<'a> { + services: Services<'a>, + shutdown_signal: Option, + autopilot_handle: JoinHandle<()>, +} + +impl<'a> TestEnv<'a> { + /// Sets up driver, autopilot (with shutdown control), and orderbook API. + async fn setup(onchain: &'a OnchainComponents, solver: TestAccount) -> Self { + colocation::start_driver( + onchain.contracts(), + vec![ + colocation::start_baseline_solver( + "test_solver".into(), + solver.clone(), + *onchain.contracts().weth.address(), + vec![], + 1, + true, + ) + .await, + ], + colocation::LiquidityProvider::UniswapV2, + false, + ); + + let services = Services::new(onchain).await; + + let (shutdown_signal, shutdown_controller) = ShutdownController::new_manual_shutdown(); + let autopilot_handle = services + .start_autopilot_with_shutdown_controller( + None, + vec![ + format!( + "--drivers=test_solver|http://localhost:11088/test_solver|{}|requested-timeout-on-problems", + const_hex::encode(solver.address()) + ), + "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver" + .to_string(), + "--gas-estimators=http://localhost:11088/gasprice".to_string(), + ], + shutdown_controller, + ) + .await; + + services + .start_api(vec![ + "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver" + .to_string(), + "--gas-estimators=http://localhost:11088/gasprice".to_string(), + ]) + .await; + + Self { + services, + shutdown_signal: Some(shutdown_signal), + autopilot_handle, + } + } + + /// Gracefully shuts down autopilot and waits for it to complete. + /// Returns the services so they can continue to be used after shutdown. + async fn shutdown_autopilot(mut self, onchain: &OnchainComponents) -> Services<'a> { + let signal = self + .shutdown_signal + .take() + .expect("shutdown already called"); + signal.shutdown(); + // Autopilot checks shutdown signal during its run loop, triggered by new blocks + onchain.mint_block().await; + tokio::time::timeout(Duration::from_secs(15), self.autopilot_handle) + .await + .expect("autopilot should shut down within timeout") + .expect("autopilot task should complete without panic"); + self.services + } +} + +/// Waits for a native price to become available and returns it. +async fn wait_for_price(services: &Services<'_>, token: &Address) -> f64 { + let price = Arc::new(Mutex::new(-1.0)); + wait_for_condition(TIMEOUT, || { + let price = price.clone(); + async move { + match services.get_native_price(token).await { + Ok(p) => { + *price.lock().await = p.price; + true + } + _ => false, + } + } + }) + .await + .expect("Expected successful price"); + *price.lock().await +} + #[tokio::test] #[ignore] async fn local_node_native_price_forwarding() { @@ -20,15 +126,16 @@ async fn local_node_native_price_forwarding() { /// /// Architecture being tested: /// User -> Orderbook (/api/v1/token/{token}/native_price) -/// -> Forwarder (configured in orderbook via `--native-price-estimators`) -/// -> Autopilot (/native_price/:token at port 12088) -/// -> Driver-based estimation +/// -> Forwarder (configured in orderbook via +/// `--native-price-estimators`) -> Autopilot (/native_price/:token at +/// port 12088) -> Driver-based estimation /// -> Returns price /// /// The forwarding chain is configured in `crates/e2e/src/setup/services.rs`: -/// - Orderbook uses `Forwarder|http://localhost:12088` (see `api_autopilot_arguments`) -/// - Autopilot uses `Driver|test_quoter|http://localhost:11088/test_solver` (see -/// `autopilot_arguments`) +/// - Orderbook uses `Forwarder|http://localhost:12088` (see +/// `api_autopilot_arguments`) +/// - Autopilot uses `Driver|test_quoter|http://localhost:11088/test_solver` +/// (see `autopilot_arguments`) async fn native_price_forwarding(web3: Web3) { tracing::info!("Setting up chain state."); let mut onchain = OnchainComponents::deploy(web3).await; @@ -41,95 +148,17 @@ async fn native_price_forwarding(web3: Web3) { .await; tracing::info!("Starting services."); - let services = Services::new(&onchain).await; - - // Start services manually (instead of start_protocol) to get shutdown control - // over autopilot, allowing us to verify the forwarding dependency. - colocation::start_driver( - onchain.contracts(), - vec![ - colocation::start_baseline_solver( - "test_solver".into(), - solver.clone(), - *onchain.contracts().weth.address(), - vec![], - 1, - true, - ) - .await, - ], - colocation::LiquidityProvider::UniswapV2, - false, - ); - - let (shutdown_signal, shutdown_controller) = ShutdownController::new_manual_shutdown(); - let autopilot_handle = services - .start_autopilot_with_shutdown_controller( - None, - vec![ - format!( - "--drivers=test_solver|http://localhost:11088/test_solver|{}|requested-timeout-on-problems", - const_hex::encode(solver.address()) - ), - "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver" - .to_string(), - "--gas-estimators=http://localhost:11088/gasprice".to_string(), - ], - shutdown_controller, - ) - .await; - - services - .start_api(vec![ - "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver".to_string(), - "--gas-estimators=http://localhost:11088/gasprice".to_string(), - ]) - .await; + let env = TestEnv::setup(&onchain, solver).await; // Test 1: Token with liquidity returns a valid price via forwarding chain tracing::info!("Testing native price for token with liquidity."); - let price_value = Arc::new(Mutex::new(-1.0)); - wait_for_condition(TIMEOUT, || async { - match services.get_native_price(token.address()).await { - Ok(p) => { - *price_value.lock().unwrap() = p.price; - true - } - _ => false, - } - }) - .await - .expect("Expected successful price for token with liquidity"); - - let price = *price_value.lock().unwrap(); - assert!( - price > 0.0, // TODO: can we use a "close enough" approximation here, like we do for the weth case below? Just comparing for greater than zero seems lazy - "Price should have been set to a positive value" - ); + let price = wait_for_price(&env.services, token.address()).await; + assert!(price > 0.0, "Price should be positive, got {}", price); tracing::info!(price, "Got native price for token"); // Test 2: WETH (native token) returns price of ~1.0 tracing::info!("Testing native price for WETH."); - let weth_price_value = Arc::new(Mutex::new(-1.0)); - wait_for_condition(TIMEOUT, || async { - match services - .get_native_price(onchain.contracts().weth.address()) - .await - { - Ok(p) => { - *weth_price_value.lock().unwrap() = p.price; - true - } - _ => false, - } - }) - .await - .expect("Expected successful price for WETH"); - - let weth_price = *weth_price_value.lock().unwrap(); - assert!(weth_price >= 0.0, "WETH price should have been set"); - - // WETH price should be ~1.0, since it is the native token + let weth_price = wait_for_price(&env.services, onchain.contracts().weth.address()).await; assert!( (weth_price - 1.0).abs() < 1e-6, "WETH price should be ~1.0, got {}", @@ -137,15 +166,10 @@ async fn native_price_forwarding(web3: Web3) { ); tracing::info!(weth_price, "Got native price for WETH"); - // Test 3: Stop autopilot and verify price estimation fails (proves forwarding dependency) + // Test 3: Stop autopilot and verify price estimation fails (proves forwarding + // dependency) tracing::info!("Stopping autopilot to verify forwarding dependency."); - shutdown_signal.shutdown(); - // Autopilot checks shutdown signal during its run loop, which is triggered by new blocks - onchain.mint_block().await; - tokio::time::timeout(Duration::from_secs(15), autopilot_handle) - .await - .expect("autopilot should shut down within timeout") - .expect("autopilot task should complete without panic"); + let services = env.shutdown_autopilot(&onchain).await; // Wait for native price cache to expire (configured as 2s in services.rs) tracing::info!("Waiting for native price cache to expire."); From 3907b6dab521c181bfa83c068d3ab400ec425569 Mon Sep 17 00:00:00 2001 From: tilacog Date: Tue, 30 Dec 2025 14:19:56 -0300 Subject: [PATCH 4/4] minor changes: fmt, comments, assertions --- crates/e2e/tests/e2e/native_price.rs | 110 ++++++++++++++++++--------- 1 file changed, 72 insertions(+), 38 deletions(-) diff --git a/crates/e2e/tests/e2e/native_price.rs b/crates/e2e/tests/e2e/native_price.rs index 8d4645fa41..515e35cb19 100644 --- a/crates/e2e/tests/e2e/native_price.rs +++ b/crates/e2e/tests/e2e/native_price.rs @@ -25,20 +25,27 @@ struct TestEnv<'a> { impl<'a> TestEnv<'a> { /// Sets up driver, autopilot (with shutdown control), and orderbook API. + /// + /// NOTE: This setup explicitly specifies configuration values required for + /// its assertions, even when they match the current defaults in the + /// Services infrastructure. This intentional redundancy decouples the + /// test's correctness from the default configuration. If defaults + /// change in the future, this test will (hopefully) continue to validate + /// the same behavior without silent breakage. async fn setup(onchain: &'a OnchainComponents, solver: TestAccount) -> Self { + // Start the driver service + let test_solver = colocation::start_baseline_solver( + "test_solver".into(), + solver.clone(), + *onchain.contracts().weth.address(), + vec![], + 1, + true, + ) + .await; colocation::start_driver( onchain.contracts(), - vec![ - colocation::start_baseline_solver( - "test_solver".into(), - solver.clone(), - *onchain.contracts().weth.address(), - vec![], - 1, - true, - ) - .await, - ], + vec![test_solver], colocation::LiquidityProvider::UniswapV2, false, ); @@ -46,27 +53,37 @@ impl<'a> TestEnv<'a> { let services = Services::new(onchain).await; let (shutdown_signal, shutdown_controller) = ShutdownController::new_manual_shutdown(); + + // Repeating the standard configuration matching `start_protocol()` in + // services.rs for a minimal working setup. + // + // TODO: Maybe we should make this configurable from the outside? let autopilot_handle = services .start_autopilot_with_shutdown_controller( None, vec![ + // Register the test solver format!( "--drivers=test_solver|http://localhost:11088/test_solver|{}|requested-timeout-on-problems", const_hex::encode(solver.address()) ), - "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver" - .to_string(), + // Configure driver-based price estimation (points to the same solver endpoint for quotes) + "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver".to_string(), + // Configure where to get gas price estimates "--gas-estimators=http://localhost:11088/gasprice".to_string(), ], shutdown_controller, ) .await; + // Start the orderbook API service services .start_api(vec![ "--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver" .to_string(), "--gas-estimators=http://localhost:11088/gasprice".to_string(), + "--native-price-cache-max-age=2s".to_string(), + "--native-price-estimators=Forwarder|http://localhost:12088".to_string(), ]) .await; @@ -101,12 +118,11 @@ async fn wait_for_price(services: &Services<'_>, token: &Address) -> f64 { wait_for_condition(TIMEOUT, || { let price = price.clone(); async move { - match services.get_native_price(token).await { - Ok(p) => { - *price.lock().await = p.price; - true - } - _ => false, + if let Ok(p) = services.get_native_price(token).await { + *price.lock().await = p.price; + true + } else { + false } } }) @@ -125,24 +141,24 @@ async fn local_node_native_price_forwarding() { /// correctly. /// /// Architecture being tested: -/// User -> Orderbook (/api/v1/token/{token}/native_price) -/// -> Forwarder (configured in orderbook via -/// `--native-price-estimators`) -> Autopilot (/native_price/:token at -/// port 12088) -> Driver-based estimation -/// -> Returns price /// -/// The forwarding chain is configured in `crates/e2e/src/setup/services.rs`: -/// - Orderbook uses `Forwarder|http://localhost:12088` (see -/// `api_autopilot_arguments`) -/// - Autopilot uses `Driver|test_quoter|http://localhost:11088/test_solver` -/// (see `autopilot_arguments`) +/// User Request +/// -> Orderbook (port 8080, /api/v1/token/{token}/native_price) +/// -> Forwarder (configured in orderbook via `--native-price-estimators`) +/// -> Autopilot (port 12088, /native_price/:token) +/// -> Driver (port 11088, /test_solver) +/// -> Returns price +/// +/// Two-hop forwarding chain: +/// - Hop 1: Orderbook → Autopilot (by `Forwarder|http://localhost:12088`) +/// - Hop 2: Autopilot → Driver (by ``Driver|test_quoter|http://localhost:11088/test_solver``) async fn native_price_forwarding(web3: Web3) { tracing::info!("Setting up chain state."); let mut onchain = OnchainComponents::deploy(web3).await; let [solver] = onchain.make_solvers(10u64.eth()).await; - // Deploy token WITH UniV2 pool - this creates liquidity so price can be - // estimated + // Deploy token WITH UniV2 pool. + // This creates liquidity so price can ben estimated. let [token] = onchain .deploy_tokens_with_weth_uni_v2_pools(500u64.eth(), 1_000u64.eth()) .await; @@ -166,22 +182,40 @@ async fn native_price_forwarding(web3: Web3) { ); tracing::info!(weth_price, "Got native price for WETH"); - // Test 3: Stop autopilot and verify price estimation fails (proves forwarding - // dependency) + // Test 3: Stop autopilot and verify price estimation fails. + // By stopping autopilot and showing that native price requests fail, we prove + // the following: + // - Orderbook depends on autopilot for native prices + // - The Forwarder configuration is actually being used + // - There's no fallback mechanism that would mask configuration issues: + // - If someone accidentally added a fallback estimator, this test would catch + // it (because the request would succeed) + tracing::info!("Stopping autopilot to verify forwarding dependency."); let services = env.shutdown_autopilot(&onchain).await; - // Wait for native price cache to expire (configured as 2s in services.rs) + // Wait for native price cache to expire (explicitly configured as 2s in this + // test setup) tracing::info!("Waiting for native price cache to expire."); tokio::time::sleep(Duration::from_secs(3)).await; tracing::info!("Verifying native price fails without autopilot."); let result = services.get_native_price(token.address()).await; + let (status, body) = + result.expect_err("Expected price request to fail after autopilot shutdown"); + + // EstimatorInternal errors (connection refused) are mapped to 404 "NoLiquidity" + // in the orderbook API (see crates/orderbook/src/api.rs:381-383) + assert!( + status == reqwest::StatusCode::NOT_FOUND, + "Expected 404 status after autopilot shutdown, got {}", + status + ); assert!( - result.is_err(), - "Expected price request to fail after autopilot shutdown, proving the forwarding \ - dependency. Got: {:?}", - result + body.contains("NoLiquidity"), + "Expected NoLiquidity error after autopilot shutdown, got: {}", + body ); + tracing::info!("Confirmed: orderbook forwards native price requests to autopilot"); }