Skip to content
177 changes: 175 additions & 2 deletions crates/e2e/tests/e2e/place_order_with_quote.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
use {
::alloy::primitives::U256,
autopilot::{
config::{
Configuration,
solver::{Account, Solver},
},
shutdown_controller::ShutdownController,
},
driver::domain::eth::NonZeroU256,
e2e::setup::*,
e2e::setup::{colocation, wait_for_condition, *},
ethrpc::alloy::{CallBuilderExt, EvmProviderExt},
model::{
order::{OrderCreation, OrderKind},
Expand All @@ -10,7 +17,8 @@ use {
},
number::units::EthUnit,
shared::web3::Web3,
std::ops::DerefMut,
std::{ops::DerefMut, str::FromStr},
url::Url,
};

#[tokio::test]
Expand All @@ -25,6 +33,12 @@ async fn local_node_disabled_same_sell_and_buy_token_order_feature() {
run_test(disabled_same_sell_and_buy_token_order_feature).await;
}

#[tokio::test]
#[ignore]
async fn local_node_fallback_native_price_estimator() {
run_test(fallback_native_price_estimator).await;
}

async fn place_order_with_quote(web3: Web3) {
let mut onchain = OnchainComponents::deploy(web3.clone()).await;

Expand Down Expand Up @@ -162,3 +176,162 @@ async fn disabled_same_sell_and_buy_token_order_feature(web3: Web3) {
matches!(services.submit_quote(&quote_request).await, Err((reqwest::StatusCode::BAD_REQUEST, response)) if response.contains("SameBuyAndSellToken"))
);
}

async fn fallback_native_price_estimator(web3: Web3) {
let mut onchain = OnchainComponents::deploy(web3.clone()).await;

let [solver] = onchain.make_solvers(10u64.eth()).await;
let [trader] = onchain.make_accounts(10u64.eth()).await;
let [token] = onchain
.deploy_tokens_with_weth_uni_v2_pools(1_000u64.eth(), 1_000u64.eth())
.await;

onchain
.contracts()
.weth
.approve(onchain.contracts().allowance, 6u64.eth())
.from(trader.address())
.send_and_watch()
.await
.unwrap();
onchain
.contracts()
.weth
.deposit()
.from(trader.address())
.value(6u64.eth())
.send_and_watch()
.await
.unwrap();

tracing::info!("Starting services.");
let services = Services::new(&onchain).await;

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 (manual_shutdown, control) = ShutdownController::new_manual_shutdown();
let autopilot_config_file = Configuration {
drivers: vec![Solver::new(
"test_solver".to_string(),
Url::from_str("http://localhost:11088/test_solver").unwrap(),
Account::Address(solver.address()),
)],
}
.to_temp_path();
let autopilot_handle = services
.start_autopilot_with_shutdown_controller(
None,
vec![
format!("--config={}", autopilot_config_file.path().display()),
"--price-estimation-drivers=test_quoter|http://localhost:11088/test_solver"
.to_string(),
"--gas-estimators=http://localhost:11088/gasprice".to_string(),
],
control,
)
.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(),
"--native-price-estimators-fallback=Driver|test_quoter|http://localhost:11088/test_solver"
.to_string(),
"--native-price-cache-max-age=2s".to_string(),
])
.await;

tracing::info!("Quoting with autopilot running");
let quote_sell_amount = 1u64.eth();
let quote_request = OrderQuoteRequest {
from: trader.address(),
sell_token: *onchain.contracts().weth.address(),
buy_token: *token.address(),
side: OrderQuoteSide::Sell {
sell_amount: SellAmount::BeforeFee {
value: NonZeroU256::try_from(quote_sell_amount).unwrap(),
},
},
..Default::default()
};
let quote_response = services.submit_quote(&quote_request).await.unwrap();
tracing::debug!(?quote_response);
assert!(quote_response.id.is_some());

tracing::info!("Placing order with autopilot running");
let order = OrderCreation {
quote_id: quote_response.id,
sell_token: *onchain.contracts().weth.address(),
sell_amount: quote_sell_amount,
buy_token: *token.address(),
buy_amount: quote_response.quote.buy_amount,
valid_to: model::time::now_in_epoch_seconds() + 300,
kind: OrderKind::Sell,
..Default::default()
}
.sign(
EcdsaSigningScheme::Eip712,
&onchain.contracts().domain_separator,
&trader.signer,
);
services.create_order(&order).await.unwrap();

tracing::info!("Shutting down autopilot");
manual_shutdown.shutdown();
wait_for_condition(TIMEOUT, || async {
onchain.mint_block().await;
autopilot_handle.is_finished()
})
.await
.unwrap();

// Wait for native price cache to expire (max age = 2s)
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
Comment thread
squadgazzz marked this conversation as resolved.

// The FallbackNativePriceEstimator switches to fallback after 3 consecutive
// ProtocolInternal errors from the primary (forwarder → dead autopilot).
tracing::info!("Waiting for native price fallback to activate");
wait_for_condition(TIMEOUT, || async {
services.get_native_price(token.address()).await.is_ok()
})
.await
.unwrap();

tracing::info!("Quoting after autopilot shutdown (via fallback)");
let quote_response = services.submit_quote(&quote_request).await.unwrap();
tracing::debug!(?quote_response);
assert!(quote_response.id.is_some());

tracing::info!("Placing order after autopilot shutdown (via fallback)");
let order = OrderCreation {
quote_id: quote_response.id,
sell_token: *onchain.contracts().weth.address(),
sell_amount: quote_sell_amount,
buy_token: *token.address(),
buy_amount: quote_response.quote.buy_amount,
valid_to: model::time::now_in_epoch_seconds() + 300,
kind: OrderKind::Sell,
..Default::default()
}
.sign(
EcdsaSigningScheme::Eip712,
&onchain.contracts().domain_separator,
&trader.signer,
);
services.create_order(&order).await.unwrap();
}
10 changes: 10 additions & 0 deletions crates/orderbook/src/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ pub struct Arguments {
#[clap(long, env)]
pub native_price_estimators: NativePriceEstimators,

/// Fallback native price estimators to use when all primary estimators
/// are down.
#[clap(long, env)]
pub native_price_estimators_fallback: Option<NativePriceEstimators>,

/// How many successful price estimates for each order will cause a fast
/// or native price estimation to return its result early.
/// The bigger the value the more the fast price estimation performs like
Expand Down Expand Up @@ -173,6 +178,7 @@ impl std::fmt::Display for Arguments {
banned_users_max_cache_size,
eip1271_skip_creation_validation,
native_price_estimators,
native_price_estimators_fallback,
fast_price_estimation_results_required,
max_limit_orders_per_user,
ipfs_gateway,
Expand Down Expand Up @@ -218,6 +224,10 @@ impl std::fmt::Display for Arguments {
"eip1271_skip_creation_validation: {eip1271_skip_creation_validation}"
)?;
writeln!(f, "native_price_estimators: {native_price_estimators}")?;
writeln!(
f,
"native_price_estimators_fallback: {native_price_estimators_fallback:?}"
)?;
writeln!(
f,
"fast_price_estimation_results_required: {fast_price_estimation_results_required}"
Expand Down
33 changes: 26 additions & 7 deletions crates/orderbook/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use {
PriceEstimating,
QuoteVerificationMode,
factory::{self, PriceEstimatorFactory},
native::NativePriceEstimating,
native::{FallbackNativePriceEstimator, NativePriceEstimating},
},
signature_validator,
token_info::{CachedTokenInfoFetcher, TokenInfoFetcher},
Expand Down Expand Up @@ -238,14 +238,33 @@ pub async fn run(args: Arguments) {
args.price_estimation.native_price_cache_max_age,
prices,
);
let primary = price_estimator_factory
.native_price_estimator(
args.native_price_estimators.as_slice(),
args.fast_price_estimation_results_required,
&native_token,
)
.await
.expect("failed to build primary native price estimator");

let inner: Box<dyn NativePriceEstimating> =
if let Some(ref fallback_config) = args.native_price_estimators_fallback {
let fallback = price_estimator_factory
.native_price_estimator(
fallback_config.as_slice(),
args.fast_price_estimation_results_required,
&native_token,
)
.await
.expect("failed to build fallback native price estimator");
Box::new(FallbackNativePriceEstimator::new(primary, fallback))
} else {
primary
};

let native_price_estimator: Arc<dyn NativePriceEstimating> = Arc::new(
price_estimator_factory
.caching_native_price_estimator(
args.native_price_estimators.as_slice(),
args.fast_price_estimation_results_required,
&native_token,
cache,
)
.caching_native_price_estimator_from_inner(inner, cache)
.await,
);

Expand Down
11 changes: 11 additions & 0 deletions crates/shared/src/price_estimation/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,17 @@ impl<'a> PriceEstimatorFactory<'a> {
.native_price_estimator(native, results_required, weth)
.await
.expect("failed to build native price estimator");
self.caching_native_price_estimator_from_inner(inner, cache)
.await
}

/// Creates a [`CachingNativePriceEstimator`] from a pre-built inner
/// estimator.
pub async fn caching_native_price_estimator_from_inner(
&mut self,
inner: Box<dyn NativePriceEstimating>,
cache: native_price_cache::Cache,
) -> native_price_cache::CachingNativePriceEstimator {
let approximation_tokens = self
.build_approximation_tokens()
.await
Expand Down
Loading
Loading