diff --git a/Cargo.lock b/Cargo.lock index ffdab7f40b..d21ec261b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3363,30 +3363,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "headers" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" -dependencies = [ - "base64 0.21.7", - "bytes", - "headers-core", - "http 0.2.12", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http 0.2.12", -] - [[package]] name = "heck" version = "0.4.1" @@ -4230,16 +4206,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4582,7 +4548,6 @@ dependencies = [ "tracing-opentelemetry", "tracing-serde", "tracing-subscriber", - "warp", ] [[package]] @@ -4742,6 +4707,7 @@ dependencies = [ "anyhow", "app-data", "async-trait", + "axum", "bigdecimal", "cached", "chain", @@ -4775,10 +4741,11 @@ dependencies = [ "thiserror 1.0.69", "tikv-jemallocator", "tokio", + "tower 0.4.13", + "tower-http 0.4.4", "tracing", "url", "vergen", - "warp", ] [[package]] @@ -5870,12 +5837,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -7406,12 +7367,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" -[[package]] -name = "unicase" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" - [[package]] name = "unicode-bidi" version = "0.3.18" @@ -7565,32 +7520,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "warp" -version = "0.3.7" -source = "git+https://github.com/cowprotocol/warp.git?rev=586244e#586244eabb564b9f9573436ee0e23edfc73f4861" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "headers", - "http 0.2.12", - "hyper 0.14.32", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project", - "scoped-tls", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-util", - "tower-service", - "tracing", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 773660c1ac..7874310aaa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,6 @@ tokio-stream = { version = "0.1.15", features = ["sync"] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.19", features = ["json"] } url = "2.5.0" -warp = { git = 'https://github.com/cowprotocol/warp.git', rev = "586244e", default-features = false } app-data = { path = "crates/app-data" } arc-swap = "1.7.1" async-stream = "0.3.5" diff --git a/crates/driver/src/infra/api/routes/healthz.rs b/crates/driver/src/infra/api/routes/healthz.rs index 157f557a93..62fc74b469 100644 --- a/crates/driver/src/infra/api/routes/healthz.rs +++ b/crates/driver/src/infra/api/routes/healthz.rs @@ -1,9 +1,13 @@ -use axum::{http::StatusCode, response::IntoResponse, routing::get}; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + routing::get, +}; pub(in crate::infra::api) fn healthz(app: axum::Router<()>) -> axum::Router<()> { app.route("/healthz", get(route)) } -async fn route() -> impl IntoResponse { - StatusCode::OK +async fn route() -> Response { + StatusCode::OK.into_response() } diff --git a/crates/driver/src/tests/setup/orderbook.rs b/crates/driver/src/tests/setup/orderbook.rs index 440b6ec9cf..8b3da45a3a 100644 --- a/crates/driver/src/tests/setup/orderbook.rs +++ b/crates/driver/src/tests/setup/orderbook.rs @@ -7,7 +7,7 @@ use { Router, extract::Path, http::StatusCode, - response::IntoResponse, + response::{IntoResponse, Response}, routing::get, }, std::{collections::HashMap, net::SocketAddr}, @@ -59,7 +59,7 @@ impl Orderbook { async fn app_data_handler( Path(app_data): Path, Extension(app_data_storage): Extension>, - ) -> impl IntoResponse { + ) -> Response { tracing::debug!("Orderbook received an app_data request: {}", app_data); let app_data_hash = match app_data.parse::() { diff --git a/crates/e2e/src/setup/proxy.rs b/crates/e2e/src/setup/proxy.rs index 9173213c26..85474f8ae3 100644 --- a/crates/e2e/src/setup/proxy.rs +++ b/crates/e2e/src/setup/proxy.rs @@ -12,7 +12,12 @@ //! cluster. use { - axum::{Router, body::Body, http::Request, response::IntoResponse}, + axum::{ + Router, + body::Body, + http::Request, + response::{IntoResponse, Response}, + }, hyper::body::to_bytes, std::{collections::VecDeque, net::SocketAddr, sync::Arc}, tokio::{sync::RwLock, task::JoinHandle}, @@ -109,7 +114,7 @@ async fn handle_request( client: reqwest::Client, state: ProxyState, req: Request, -) -> impl IntoResponse { +) -> Response { let (parts, body) = req.into_parts(); // Convert body to bytes once for reuse across retries diff --git a/crates/e2e/tests/e2e/malformed_requests.rs b/crates/e2e/tests/e2e/malformed_requests.rs index 8dc9c09fab..c0889f60a2 100644 --- a/crates/e2e/tests/e2e/malformed_requests.rs +++ b/crates/e2e/tests/e2e/malformed_requests.rs @@ -53,8 +53,8 @@ async fn http_validation(web3: Web3) { assert_eq!( response.status(), - StatusCode::NOT_FOUND, - "Expected 404 for invalid OrderUid ({description}): {uid}" + StatusCode::BAD_REQUEST, + "Expected 400 for invalid OrderUid ({description}): {uid}" ); } @@ -76,8 +76,8 @@ async fn http_validation(web3: Web3) { assert_eq!( response.status(), - StatusCode::NOT_FOUND, - "Expected 404 for invalid Address ({description}): {addr}" + StatusCode::BAD_REQUEST, + "Expected 400 for invalid Address ({description}): {addr}" ); } @@ -90,8 +90,8 @@ async fn http_validation(web3: Web3) { assert_eq!( response.status(), - StatusCode::NOT_FOUND, - "Expected 404 for invalid token Address ({description}): {addr}" + StatusCode::BAD_REQUEST, + "Expected 400 for invalid token Address ({description}): {addr}" ); } @@ -113,19 +113,24 @@ async fn http_validation(web3: Web3) { assert_eq!( response.status(), - StatusCode::NOT_FOUND, - "Expected 404 for invalid tx hash ({description}): {hash}" + StatusCode::BAD_REQUEST, + "Expected 400 for invalid tx hash ({description}): {hash}" ); } // Test malformed auction IDs - let invalid_auction_ids: Vec<(&str, &str)> = vec![ - ("not-a-number", "non-numeric"), - ("-1", "negative number"), - ("99999999999999999999999", "u64 overflow"), - ]; - - for (id, description) in invalid_auction_ids { + // Note: "-1" returns 404 because it doesn't match the u64 route pattern at all, + // while non-numeric strings return 400 as they match the path but fail + // deserialization + for (id, description, expected_status) in [ + ("not-a-number", "non-numeric", StatusCode::BAD_REQUEST), + ("-1", "negative number", StatusCode::NOT_FOUND), + ( + "99999999999999999999999", + "u64 overflow", + StatusCode::BAD_REQUEST, + ), + ] { let response = client .get(format!("{API_HOST}/api/v1/solver_competition/{id}")) .send() @@ -134,8 +139,8 @@ async fn http_validation(web3: Web3) { assert_eq!( response.status(), - StatusCode::NOT_FOUND, - "Expected 404 for invalid AuctionId ({description}): {id}" + expected_status, + "Expected {expected_status} for invalid AuctionId ({description}): {id}" ); } @@ -203,6 +208,7 @@ async fn http_validation(web3: Web3) { ); // Missing required fields (empty object) + // Axum returns 422 (Unprocessable Entity) for JSON deserialization errors let response = client .post(format!("{API_HOST}/api/v1/orders")) .header("Content-Type", "application/json") @@ -213,8 +219,8 @@ async fn http_validation(web3: Web3) { assert_eq!( response.status(), - StatusCode::BAD_REQUEST, - "Missing required fields should return 400" + StatusCode::UNPROCESSABLE_ENTITY, + "Missing required fields should return 422" ); // Wrong field types @@ -236,8 +242,8 @@ async fn http_validation(web3: Web3) { assert_eq!( response.status(), - StatusCode::BAD_REQUEST, - "Wrong field types should return 400" + StatusCode::UNPROCESSABLE_ENTITY, + "Wrong field types should return 422" ); // Invalid enum value @@ -256,8 +262,8 @@ async fn http_validation(web3: Web3) { assert_eq!( response.status(), - StatusCode::BAD_REQUEST, - "Invalid enum value should return 400" + StatusCode::UNPROCESSABLE_ENTITY, + "Invalid enum value should return 422" ); // Test error response formats @@ -270,7 +276,7 @@ async fn http_validation(web3: Web3) { .await .unwrap(); - assert_eq!(response.status(), StatusCode::BAD_REQUEST); + assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY); let body_text = response.text().await.unwrap(); assert!( diff --git a/crates/e2e/tests/e2e/replace_order.rs b/crates/e2e/tests/e2e/replace_order.rs index d2175f94e2..0a8706f4f6 100644 --- a/crates/e2e/tests/e2e/replace_order.rs +++ b/crates/e2e/tests/e2e/replace_order.rs @@ -23,7 +23,7 @@ fn parse_order_replacement_error(status: StatusCode, body: &str) -> Option match error.error_type { + StatusCode::BAD_REQUEST => match error.error_type.as_str() { "InvalidSignature" => Some(OrderReplacementError::InvalidSignature), "OldOrderActivelyBidOn" => Some(OrderReplacementError::OldOrderActivelyBidOn), _ => None, @@ -46,7 +46,7 @@ fn parse_order_cancellation_error( let error: ApiError = serde_json::from_str(body).ok()?; match status { - StatusCode::BAD_REQUEST => match error.error_type { + StatusCode::BAD_REQUEST => match error.error_type.as_str() { "InvalidSignature" => Some(OrderCancellationError::InvalidSignature), "AlreadyCancelled" => Some(OrderCancellationError::AlreadyCancelled), "OrderFullyExecuted" => Some(OrderCancellationError::OrderFullyExecuted), diff --git a/crates/observe/Cargo.toml b/crates/observe/Cargo.toml index 91578c049f..ee99edca35 100644 --- a/crates/observe/Cargo.toml +++ b/crates/observe/Cargo.toml @@ -26,7 +26,6 @@ tracing = { workspace = true } tracing-opentelemetry = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt", "time"] } tracing-serde = { workspace = true } -warp = { workspace = true } jemalloc_pprof = { workspace = true } [lints] diff --git a/crates/observe/src/distributed_tracing/mod.rs b/crates/observe/src/distributed_tracing/mod.rs index 03d8ca7d3c..6bb9302c14 100644 --- a/crates/observe/src/distributed_tracing/mod.rs +++ b/crates/observe/src/distributed_tracing/mod.rs @@ -1,4 +1,3 @@ pub mod request_id; pub mod trace_id_format; pub mod tracing_axum; -pub mod tracing_warp; diff --git a/crates/observe/src/distributed_tracing/tracing_warp.rs b/crates/observe/src/distributed_tracing/tracing_warp.rs deleted file mode 100644 index 7688397081..0000000000 --- a/crates/observe/src/distributed_tracing/tracing_warp.rs +++ /dev/null @@ -1,23 +0,0 @@ -use { - crate::{distributed_tracing::request_id::request_id, tracing::HeaderExtractor}, - opentelemetry::global, - tracing::info, - tracing_opentelemetry::OpenTelemetrySpanExt, - warp::http::HeaderMap, -}; - -pub fn make_span(info: warp::trace::Info) -> tracing::Span { - let headers: &HeaderMap = info.request_headers(); - - // Extract OTEL context from headers - let parent_cx = global::get_text_map_propagator(|prop| prop.extract(&HeaderExtractor(headers))); - - let span = tracing::info_span!("http_request", request_id = %request_id(headers)); - span.set_parent(parent_cx); // sets parent context for distributed trace - { - let _span = span.enter(); - info!(method = %info.method(), path = %info.path(), "HTTP request"); - } - - span -} diff --git a/crates/orderbook/Cargo.toml b/crates/orderbook/Cargo.toml index 1f26181049..936b89986d 100644 --- a/crates/orderbook/Cargo.toml +++ b/crates/orderbook/Cargo.toml @@ -20,6 +20,7 @@ alloy = { workspace = true } anyhow = { workspace = true } app-data = { workspace = true } async-trait = { workspace = true } +axum = { workspace = true } bigdecimal = { workspace = true } cached = { workspace = true } chain = { workspace = true } @@ -52,9 +53,10 @@ strum = { workspace = true } sqlx = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "sync", "time"] } +tower = { workspace = true } +tower-http = { workspace = true, features = ["cors", "trace"] } tracing = { workspace = true } url = { workspace = true } -warp = { workspace = true } [dev-dependencies] mockall = { workspace = true } diff --git a/crates/orderbook/openapi.yml b/crates/orderbook/openapi.yml index 79a07a488a..9434a62d98 100644 --- a/crates/orderbook/openapi.yml +++ b/crates/orderbook/openapi.yml @@ -81,6 +81,8 @@ paths: description: "Forbidden, your account is deny-listed." "404": description: No route was found quoting the order. + "422": + description: Unable to parse request body as valid JSON. "429": description: Too many order placements. "500": @@ -121,6 +123,8 @@ paths: description: Invalid signature. "404": description: One or more orders were not found and no orders were cancelled. + "422": + description: Unable to parse request body as valid JSON. "/api/v1/orders/{UID}": get: operationId: getOrder @@ -177,6 +181,8 @@ paths: description: Invalid signature. "404": description: Order was not found. + "422": + description: Unable to parse request body as valid JSON. "/api/v1/orders/{UID}/status": get: operationId: getOrderStatus @@ -437,6 +443,8 @@ paths: $ref: "#/components/schemas/PriceEstimationError" "404": description: No route was found for the specified order. + "422": + description: Unable to parse request body as valid JSON. "429": description: Too many order quotes. "500": @@ -626,6 +634,8 @@ paths: $ref: "#/components/schemas/AppDataHash" "400": description: Error validating full `appData` + "422": + description: Unable to parse request body as valid JSON. "500": description: Error storing the full `appData` /api/v1/app_data: @@ -657,6 +667,8 @@ paths: $ref: "#/components/schemas/AppDataHash" "400": description: Error validating full `appData` + "422": + description: Unable to parse request body as valid JSON. "500": description: Error storing the full `appData` "/api/v1/users/{address}/total_surplus": @@ -1643,7 +1655,9 @@ components: - UnsupportedToken - InvalidAppData - AppDataHashMismatch + - AppDataMismatch - AppdataFromMismatch + - MetadataSerializationFailed - OldOrderActivelyBidOn description: type: string @@ -1676,7 +1690,7 @@ components: enum: - QuoteNotVerified - UnsupportedToken - - ZeroAmount + - NoLiquidity - UnsupportedOrderType description: type: string diff --git a/crates/orderbook/src/api.rs b/crates/orderbook/src/api.rs index 0fa277a975..c91842cdfa 100644 --- a/crates/orderbook/src/api.rs +++ b/crates/orderbook/src/api.rs @@ -1,23 +1,21 @@ use { crate::{app_data, database::Postgres, orderbook::Orderbook, quoter::QuoteHandler}, - anyhow::Result, - observe::distributed_tracing::tracing_warp::make_span, - serde::{Deserialize, Serialize, de::DeserializeOwned}, + axum::{ + Router, + extract::DefaultBodyLimit, + http::{Request, StatusCode}, + middleware::{self, Next}, + response::{IntoResponse, Json, Response}, + }, + observe::distributed_tracing::tracing_axum, + serde::{Deserialize, Serialize}, shared::price_estimation::{PriceEstimationError, native::NativePriceEstimating}, std::{ - convert::Infallible, fmt::Debug, sync::Arc, time::{Duration, Instant}, }, - warp::{ - Filter, - Rejection, - Reply, - filters::BoxedFilter, - hyper::StatusCode, - reply::{Json, WithStatus, json, with_status}, - }, + tower_http::{cors::CorsLayer, trace::TraceLayer}, }; mod cancel_order; @@ -40,6 +38,57 @@ mod post_quote; mod put_app_data; mod version; +/// Centralized application state shared across all API handlers +#[derive(Clone)] +pub struct AppState { + pub database_write: Postgres, + pub database_read: Postgres, + pub orderbook: Arc, + pub quotes: Arc, + pub app_data: Arc, + pub native_price_estimator: Arc, + pub quote_timeout: Duration, +} + + +/// Middleware that automatically tracks metrics using Axum's MatchedPath +async fn with_matched_path_metric( + req: Request, + next: Next, +) -> Response { + let metrics = ApiMetrics::instance(observe::metrics::get_storage_registry()).unwrap(); + + // Extract matched path and HTTP method + let method = req.method().to_string(); + let matched_path = req + .extensions() + .get::() + .map(|path| path.as_str()) + .unwrap_or("unknown"); + + // Create label in format "METHOD /path" + let label = format!("{} {}", method, matched_path); + + let timer = Instant::now(); + let response = next.run(req).await; + let status = response.status(); + + // Track completed requests + metrics.on_request_completed(&label, status, timer); + + // Track rejected requests (4xx and 5xx status codes) + if status.is_client_error() || status.is_server_error() { + metrics + .requests_rejected + .with_label_values(&[status.as_str()]) + .inc(); + } + + response +} + +const MAX_JSON_BODY_PAYLOAD: u64 = 1024 * 16; + pub fn handle_all_routes( database_write: Postgres, database_read: Postgres, @@ -48,118 +97,128 @@ pub fn handle_all_routes( app_data: Arc, native_price_estimator: Arc, quote_timeout: Duration, -) -> impl Filter + Clone { - // Note that we add a string with endpoint's name to all responses. - // This string will be used later to report metrics. - // It is not used to form the actual server response. - - let routes = vec![ - ( - "v1/create_order", - box_filter(post_order::post_order(orderbook.clone())), - ), - ( - "v1/get_order", - box_filter(get_order_by_uid::get_order_by_uid(orderbook.clone())), - ), - ( - "v1/get_order_status", - box_filter(get_order_status::get_status(orderbook.clone())), - ), - ( - "v1/get_trades", - box_filter(get_trades::get_trades(database_read.clone())), - ), - ( - "v2/get_trades", - box_filter(get_trades_v2::get_trades(database_read.clone())), - ), - ( - "v1/cancel_order", - box_filter(cancel_order::cancel_order(orderbook.clone())), - ), - ( - "v1/cancel_orders", - box_filter(cancel_orders::filter(orderbook.clone())), - ), - ( - "v1/get_user_orders", - box_filter(get_user_orders::get_user_orders(orderbook.clone())), - ), - ( - "v1/get_orders_by_tx", - box_filter(get_orders_by_tx::get_orders_by_tx(orderbook.clone())), - ), - ("v1/post_quote", box_filter(post_quote::post_quote(quotes))), - ( - "v1/auction", - box_filter(get_auction::get_auction(orderbook.clone())), - ), - ( - "v1/solver_competition", - box_filter(get_solver_competition::get(Arc::new( - database_write.clone(), - ))), - ), - ( - "v2/solver_competition", - box_filter(get_solver_competition_v2::get(database_write.clone())), - ), - ( - "v1/solver_competition/latest", - box_filter(get_solver_competition::get_latest(Arc::new( - database_write.clone(), - ))), - ), - ( - "v2/solver_competition/latest", - box_filter(get_solver_competition_v2::get_latest( - database_write.clone(), - )), - ), - ("v1/version", box_filter(version::version())), - ( - "v1/get_native_price", - box_filter(get_native_price::get_native_price( - native_price_estimator, - quote_timeout, - )), - ), - ( - "v1/get_app_data", - get_app_data::get(database_read.clone()).boxed(), - ), - ( - "v1/put_app_data", - box_filter(put_app_data::filter(app_data)), - ), - ( - "v1/get_total_surplus", - box_filter(get_total_surplus::get(database_read.clone())), - ), - ( - "v1/get_token_metadata", - box_filter(get_token_metadata::get_token_metadata(database_read)), - ), - ]; - - finalize_router(routes, "orderbook::api::request_summary") -} - -pub type ApiReply = WithStatus; - -// We turn Rejection into Reply to workaround warp not setting CORS headers on -// rejections. -async fn handle_rejection(err: Rejection) -> Result { - let response = err.default_response(); - +) -> Router { + // Capture app_data size limit before moving it into state + let app_data_size_limit = app_data.size_limit(); + + let state = Arc::new(AppState { + database_write, + database_read, + orderbook, + quotes, + app_data, + native_price_estimator, + quote_timeout, + }); + + // Initialize metrics let metrics = ApiMetrics::instance(observe::metrics::get_storage_registry()).unwrap(); - metrics - .requests_rejected - .with_label_values(&[response.status().as_str()]) - .inc(); + metrics.reset_requests_rejected(); - Ok(response) + let api_router = Router::new() + // V1 routes + .route( + "/v1/account/:owner/orders", + axum::routing::get(get_user_orders::get_user_orders_handler) + ) + .route( + "/v1/app_data", + axum::routing::put(put_app_data::put_app_data_without_hash) + .layer(DefaultBodyLimit::max(app_data_size_limit)) + ) + .route( + "/v1/app_data/:hash", + axum::routing::get(get_app_data::get_app_data_handler) + .merge( + axum::routing::put(put_app_data::put_app_data_with_hash) + .layer(DefaultBodyLimit::max(app_data_size_limit)) + ), + ) + .route( + "/v1/auction", + axum::routing::get(get_auction::get_auction_handler) + ) + .route( + "/v1/orders", + axum::routing::post(post_order::post_order_handler) + .merge( + axum::routing::delete(cancel_orders::cancel_orders_handler) + ), + ) + .route( + "/v1/orders/:uid", + axum::routing::get(get_order_by_uid::get_order_by_uid_handler) + .merge( + axum::routing::delete(cancel_order::cancel_order_handler) + ), + ) + .route( + "/v1/orders/:uid/status", + axum::routing::get(get_order_status::get_status_handler) + ) + .route( + "/v1/quote", + axum::routing::post(post_quote::post_quote_handler) + ) + // /solver_competition routes (specific before parameterized) + .route( + "/v1/solver_competition/latest", + axum::routing::get(get_solver_competition::get_solver_competition_latest_handler) + ) + .route( + "/v1/solver_competition/by_tx_hash/:tx_hash", + axum::routing::get(get_solver_competition::get_solver_competition_by_hash_handler) + ) + .route( + "/v1/solver_competition/:auction_id", + axum::routing::get(get_solver_competition::get_solver_competition_by_id_handler) + ) + .route( + "/v1/token/:token/metadata", + axum::routing::get(get_token_metadata::get_token_metadata_handler) + ) + .route( + "/v1/token/:token/native_price", + axum::routing::get(get_native_price::get_native_price_handler) + ) + .route( + "/v1/trades", + axum::routing::get(get_trades::get_trades_handler) + ) + .route( + "/v1/transactions/:hash/orders", + axum::routing::get(get_orders_by_tx::get_orders_by_tx_handler) + ) + .route( + "/v1/users/:user/total_surplus", + axum::routing::get(get_total_surplus::get_total_surplus_handler) + ) + .route( + "/v1/version", + axum::routing::get(version::version_handler) + ) + // V2 routes + // /solver_competition routes (specific before parameterized) + .route( + "/v2/solver_competition/latest", + axum::routing::get(get_solver_competition_v2::get_solver_competition_latest_handler) + ) + .route( + "/v2/solver_competition/by_tx_hash/:tx_hash", + axum::routing::get(get_solver_competition_v2::get_solver_competition_by_hash_handler) + ) + .route( + "/v2/solver_competition/:auction_id", + axum::routing::get(get_solver_competition_v2::get_solver_competition_by_id_handler) + ) + .route( + "/v2/trades", + axum::routing::get(get_trades_v2::get_trades_handler) + ) + .with_state(state) + .layer(middleware::from_fn(with_matched_path_metric)); + + finalize_router(api_router) } #[derive(prometheus_metric_storage::MetricStorage, Clone, Debug)] @@ -200,14 +259,6 @@ impl ApiMetrics { } } - fn reset_requests_complete(&self, method: &str) { - for status in Self::INITIAL_STATUSES { - self.requests_complete - .with_label_values(&[method, status.as_str()]) - .reset(); - } - } - fn on_request_completed(&self, method: &str, status: StatusCode, timer: Instant) { self.requests_complete .with_label_values(&[method, status.as_str()]) @@ -220,23 +271,27 @@ impl ApiMetrics { #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct Error<'a> { - pub error_type: &'a str, - pub description: &'a str, +pub struct Error { + pub error_type: String, + pub description: String, /// Additional arbitrary data that can be attached to an API error. #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, } -pub fn error(error_type: &str, description: impl AsRef) -> Json { - json(&Error { - error_type, - description: description.as_ref(), +pub fn error(error_type: &str, description: impl AsRef) -> Json { + Json(Error { + error_type: error_type.to_string(), + description: description.as_ref().to_string(), data: None, }) } -pub fn rich_error(error_type: &str, description: impl AsRef, data: impl Serialize) -> Json { +pub fn rich_error( + error_type: &str, + description: impl AsRef, + data: impl Serialize, +) -> Json { let data = match serde_json::to_value(&data) { Ok(value) => Some(value), Err(err) => { @@ -245,146 +300,84 @@ pub fn rich_error(error_type: &str, description: impl AsRef, data: impl Ser } }; - json(&Error { - error_type, - description: description.as_ref(), + Json(Error { + error_type: error_type.to_string(), + description: description.as_ref().to_string(), data, }) } -pub fn internal_error_reply() -> ApiReply { - with_status( - error("InternalServerError", ""), +pub fn internal_error_reply() -> Response { + ( StatusCode::INTERNAL_SERVER_ERROR, + error("InternalServerError", ""), ) -} - -pub fn convert_json_response(result: Result) -> WithStatus -where - T: Serialize, - E: IntoWarpReply + Debug, -{ - match result { - Ok(response) => with_status(warp::reply::json(&response), StatusCode::OK), - Err(err) => err.into_warp_reply(), - } -} - -pub trait IntoWarpReply { - fn into_warp_reply(self) -> ApiReply; -} - -pub async fn response_body(response: warp::hyper::Response) -> Vec { - let mut body = response.into_body(); - let mut result = Vec::new(); - while let Some(bytes) = futures::StreamExt::next(&mut body).await { - result.extend_from_slice(bytes.unwrap().as_ref()); - } - result -} - -const MAX_JSON_BODY_PAYLOAD: u64 = 1024 * 16; - -pub fn extract_payload() --> impl Filter + Clone { - // (rejecting huge payloads)... - extract_payload_with_max_size(MAX_JSON_BODY_PAYLOAD) -} - -pub fn extract_payload_with_max_size( - max_size: u64, -) -> impl Filter + Clone { - warp::body::content_length_limit(max_size).and(warp::body::json()) -} - -pub type BoxedRoute = BoxedFilter<(Box,)>; - -pub fn box_filter(filter: Filter_) -> BoxedFilter<(Box,)> -where - Filter_: Filter + Send + Sync + 'static, - Reply_: Reply + Send + 'static, -{ - filter.map(|a| Box::new(a) as Box).boxed() + .into_response() } /// Sets up basic metrics, cors and proper log tracing for all routes. -/// -/// # Panics -/// -/// This method panics if `routes` is empty. -pub fn finalize_router( - routes: Vec<(&'static str, BoxedRoute)>, - log_prefix: &'static str, -) -> impl Filter + Clone { - let metrics = ApiMetrics::instance(observe::metrics::get_storage_registry()).unwrap(); - metrics.reset_requests_rejected(); - for (method, _) in &routes { - metrics.reset_requests_complete(method); - } - - let router = routes - .into_iter() - .fold( - Option::)>>::None, - |router, (method, route)| { - let route = route.map(move |result| (method, result)).untuple_one(); - let next = match router { - Some(router) => router.or(route).unify().boxed(), - None => route.boxed(), - }; - Some(next) - }, - ) - .expect("routes cannot be empty"); - - let instrumented = - warp::any() - .map(Instant::now) - .and(router) - .map(|timer, method, reply: Box| { - let response = reply.into_response(); - metrics.on_request_completed(method, response.status(), timer); - response - }); - - // Final setup - let cors = warp::cors() - .allow_any_origin() +/// Takes a router with versioned routes and nests under /api, then applies +/// middleware. +fn finalize_router(api_router: Router) -> Router { + let cors = CorsLayer::new() + .allow_origin(tower_http::cors::Any) .allow_methods(vec![ - "GET", "POST", "DELETE", "OPTIONS", "PUT", "PATCH", "HEAD", + axum::http::Method::GET, + axum::http::Method::POST, + axum::http::Method::DELETE, + axum::http::Method::OPTIONS, + axum::http::Method::PUT, + axum::http::Method::PATCH, + axum::http::Method::HEAD, ]) - .allow_headers(vec!["Origin", "Content-Type", "X-Auth-Token", "X-AppId"]); - - warp::path!("api" / ..) - .and(instrumented) - .recover(handle_rejection) - .with(cors) - .with(warp::log::log(log_prefix)) - .with(warp::trace::trace(make_span)) + .allow_headers(vec![ + axum::http::header::ORIGIN, + axum::http::header::CONTENT_TYPE, + // Must be lower case due to the HTTP-2 spec + axum::http::HeaderName::from_static("x-auth-token"), + axum::http::HeaderName::from_static("x-appid"), + ]); + + let trace_layer = TraceLayer::new_for_http().make_span_with(tracing_axum::make_span); + + Router::new() + .nest("/api", api_router) + .layer(DefaultBodyLimit::max(MAX_JSON_BODY_PAYLOAD as usize)) + .layer(cors) + .layer(trace_layer) } -impl IntoWarpReply for PriceEstimationError { - fn into_warp_reply(self) -> WithStatus { - match self { - Self::UnsupportedToken { token, reason } => with_status( +// Newtype wrapper for PriceEstimationError to allow IntoResponse implementation +// (orphan rules prevent implementing IntoResponse directly on external types) +pub(crate) struct PriceEstimationErrorWrapper(pub(crate) PriceEstimationError); + +impl IntoResponse for PriceEstimationErrorWrapper { + fn into_response(self) -> Response { + match self.0 { + PriceEstimationError::UnsupportedToken { token, reason } => ( + StatusCode::BAD_REQUEST, error( "UnsupportedToken", format!("Token {token:?} is unsupported: {reason:}"), ), + ) + .into_response(), + PriceEstimationError::UnsupportedOrderType(order_type) => ( StatusCode::BAD_REQUEST, - ), - Self::UnsupportedOrderType(order_type) => with_status( error( "UnsupportedOrderType", format!("{order_type} not supported"), ), - StatusCode::BAD_REQUEST, - ), - Self::NoLiquidity | Self::RateLimited | Self::EstimatorInternal(_) => with_status( - error("NoLiquidity", "no route found"), + ) + .into_response(), + PriceEstimationError::NoLiquidity + | PriceEstimationError::RateLimited + | PriceEstimationError::EstimatorInternal(_) => ( StatusCode::NOT_FOUND, - ), - Self::ProtocolInternal(err) => { + error("NoLiquidity", "no route found"), + ) + .into_response(), + PriceEstimationError::ProtocolInternal(err) => { tracing::error!(?err, "PriceEstimationError::Other"); internal_error_reply() } @@ -392,6 +385,29 @@ impl IntoWarpReply for PriceEstimationError { } } +// Implement From to allow easy conversion +impl From for PriceEstimationErrorWrapper { + fn from(err: PriceEstimationError) -> Self { + Self(err) + } +} + +#[cfg(test)] +pub async fn response_body(response: axum::http::Response) -> Vec +where + B: axum::body::HttpBody + Unpin, + B::Data: AsRef<[u8]>, + B::Error: Debug, +{ + let mut body = response.into_body(); + let mut result = Vec::new(); + while let Some(frame) = body.data().await { + let bytes = frame.unwrap(); + result.extend_from_slice(bytes.as_ref()); + } + result +} + #[cfg(test)] mod tests { use {super::*, serde::ser, serde_json::json}; @@ -400,8 +416,8 @@ mod tests { fn rich_errors_skip_unset_data_field() { assert_eq!( serde_json::to_value(&Error { - error_type: "foo", - description: "bar", + error_type: "foo".to_string(), + description: "bar".to_string(), data: None, }) .unwrap(), @@ -412,8 +428,8 @@ mod tests { ); assert_eq!( serde_json::to_value(Error { - error_type: "foo", - description: "bar", + error_type: "foo".to_string(), + description: "bar".to_string(), data: Some(json!(42)), }) .unwrap(), @@ -427,6 +443,8 @@ mod tests { #[tokio::test] async fn rich_errors_handle_serialization_errors() { + use axum::body::HttpBody; + struct AlwaysErrors; impl Serialize for AlwaysErrors { fn serialize(&self, _: S) -> Result @@ -437,16 +455,16 @@ mod tests { } } - let body = warp::hyper::body::to_bytes( - rich_error("foo", "bar", AlwaysErrors) - .into_response() - .into_body(), - ) - .await - .unwrap(); + let response = rich_error("foo", "bar", AlwaysErrors).into_response(); + let mut body = response.into_body(); + let mut bytes = Vec::new(); + while let Some(frame) = body.data().await { + let chunk = frame.unwrap(); + bytes.extend_from_slice(&chunk); + } assert_eq!( - serde_json::from_slice::(&body).unwrap(), + serde_json::from_slice::(&bytes).unwrap(), json!({ "errorType": "foo", "description": "bar", diff --git a/crates/orderbook/src/api/cancel_order.rs b/crates/orderbook/src/api/cancel_order.rs index ae141b299d..7ae68d32f9 100644 --- a/crates/orderbook/src/api/cancel_order.rs +++ b/crates/orderbook/src/api/cancel_order.rs @@ -1,60 +1,71 @@ use { - crate::{ - api::{IntoWarpReply, convert_json_response, extract_payload}, - orderbook::{OrderCancellationError, Orderbook}, - }, + crate::{api::AppState, orderbook::OrderCancellationError}, anyhow::Result, + axum::{ + Json, + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Response}, + }, model::order::{CancellationPayload, OrderCancellation, OrderUid}, - std::{convert::Infallible, sync::Arc}, - warp::{Filter, Rejection, hyper::StatusCode, reply::with_status}, + std::sync::Arc, }; -pub fn cancel_order_request() --> impl Filter + Clone { - warp::path!("v1" / "orders" / OrderUid) - .and(warp::delete()) - .and(extract_payload()) - .map(|uid, payload: CancellationPayload| OrderCancellation { - order_uid: uid, - signature: payload.signature, - signing_scheme: payload.signing_scheme, - }) +pub async fn cancel_order_handler( + State(state): State>, + Path(uid): Path, + Json(payload): Json, +) -> Response { + let order_cancellation = OrderCancellation { + order_uid: uid, + signature: payload.signature, + signing_scheme: payload.signing_scheme, + }; + let result = state.orderbook.cancel_order(order_cancellation).await; + cancel_order_response(result) } -impl IntoWarpReply for OrderCancellationError { - fn into_warp_reply(self) -> super::ApiReply { +impl IntoResponse for OrderCancellationError { + fn into_response(self) -> Response { match self { - Self::InvalidSignature => with_status( + Self::InvalidSignature => ( + StatusCode::BAD_REQUEST, super::error("InvalidSignature", "Malformed signature"), + ) + .into_response(), + Self::AlreadyCancelled => ( StatusCode::BAD_REQUEST, - ), - Self::AlreadyCancelled => with_status( super::error("AlreadyCancelled", "Order is already cancelled"), + ) + .into_response(), + Self::OrderFullyExecuted => ( StatusCode::BAD_REQUEST, - ), - Self::OrderFullyExecuted => with_status( super::error("OrderFullyExecuted", "Order is fully executed"), + ) + .into_response(), + Self::OrderExpired => ( StatusCode::BAD_REQUEST, - ), - Self::OrderExpired => with_status( super::error("OrderExpired", "Order is expired"), - StatusCode::BAD_REQUEST, - ), - Self::OrderNotFound => with_status( - super::error("OrderNotFound", "Order not located in database"), + ) + .into_response(), + Self::OrderNotFound => ( StatusCode::NOT_FOUND, - ), - Self::WrongOwner => with_status( + super::error("OrderNotFound", "Order not located in database"), + ) + .into_response(), + Self::WrongOwner => ( + StatusCode::UNAUTHORIZED, super::error( "WrongOwner", "Signature recovery's owner doesn't match order's", ), - StatusCode::UNAUTHORIZED, - ), - Self::OnChainOrder => with_status( - super::error("OnChainOrder", "On-chain orders must be cancelled on-chain"), + ) + .into_response(), + Self::OnChainOrder => ( StatusCode::BAD_REQUEST, - ), + super::error("OnChainOrder", "On-chain orders must be cancelled on-chain"), + ) + .into_response(), Self::Other(err) => { tracing::error!(?err, "cancel_order"); crate::api::internal_error_reply() @@ -63,20 +74,11 @@ impl IntoWarpReply for OrderCancellationError { } } -pub fn cancel_order_response(result: Result<(), OrderCancellationError>) -> super::ApiReply { - convert_json_response(result.map(|_| "Cancelled")) -} - -pub fn cancel_order( - orderbook: Arc, -) -> impl Filter + Clone { - cancel_order_request().and_then(move |order| { - let orderbook = orderbook.clone(); - async move { - let result = orderbook.cancel_order(order).await; - Result::<_, Infallible>::Ok(cancel_order_response(result)) - } - }) +pub fn cancel_order_response(result: Result<(), OrderCancellationError>) -> Response { + match result { + Ok(_) => (axum::http::StatusCode::OK, axum::Json("Cancelled")).into_response(), + Err(err) => err.into_response(), + } } #[cfg(test)] @@ -86,7 +88,6 @@ mod tests { alloy::primitives::b256, model::signature::{EcdsaSignature, EcdsaSigningScheme}, serde_json::json, - warp::{Reply, test::request}, }; #[test] @@ -111,59 +112,35 @@ mod tests { ); } - #[tokio::test] - async fn cancel_order_request_ok() { - let filter = cancel_order_request(); - let cancellation = OrderCancellation::default(); - - let request = request() - .path(&format!("/v1/orders/{}", cancellation.order_uid)) - .method("DELETE") - .header("content-type", "application/json") - .json(&CancellationPayload { - signature: cancellation.signature, - signing_scheme: cancellation.signing_scheme, - }); - let result = request.filter(&filter).await.unwrap(); - assert_eq!(result, cancellation); - } - #[test] fn cancel_order_response_ok() { - let response = cancel_order_response(Ok(())).into_response(); + let response = cancel_order_response(Ok(())); assert_eq!(response.status(), StatusCode::OK); } #[test] fn cancel_order_response_err() { - let response = - cancel_order_response(Err(OrderCancellationError::InvalidSignature)).into_response(); + let response = cancel_order_response(Err(OrderCancellationError::InvalidSignature)); assert_eq!(response.status(), StatusCode::BAD_REQUEST); - let response = - cancel_order_response(Err(OrderCancellationError::OrderFullyExecuted)).into_response(); + let response = cancel_order_response(Err(OrderCancellationError::OrderFullyExecuted)); assert_eq!(response.status(), StatusCode::BAD_REQUEST); - let response = - cancel_order_response(Err(OrderCancellationError::AlreadyCancelled)).into_response(); + let response = cancel_order_response(Err(OrderCancellationError::AlreadyCancelled)); assert_eq!(response.status(), StatusCode::BAD_REQUEST); - let response = - cancel_order_response(Err(OrderCancellationError::OrderExpired)).into_response(); + let response = cancel_order_response(Err(OrderCancellationError::OrderExpired)); assert_eq!(response.status(), StatusCode::BAD_REQUEST); - let response = - cancel_order_response(Err(OrderCancellationError::WrongOwner)).into_response(); + let response = cancel_order_response(Err(OrderCancellationError::WrongOwner)); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - let response = - cancel_order_response(Err(OrderCancellationError::OrderNotFound)).into_response(); + let response = cancel_order_response(Err(OrderCancellationError::OrderNotFound)); assert_eq!(response.status(), StatusCode::NOT_FOUND); let response = cancel_order_response(Err(OrderCancellationError::Other( anyhow::Error::msg("test error"), - ))) - .into_response(); + ))); assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); } } diff --git a/crates/orderbook/src/api/cancel_orders.rs b/crates/orderbook/src/api/cancel_orders.rs index b252fd8dae..adf89639e1 100644 --- a/crates/orderbook/src/api/cancel_orders.rs +++ b/crates/orderbook/src/api/cancel_orders.rs @@ -1,32 +1,21 @@ use { - crate::{ - api::{convert_json_response, extract_payload}, - orderbook::{OrderCancellationError, Orderbook}, + crate::api::AppState, + axum::{ + Json, + extract::State, + http::StatusCode, + response::{IntoResponse, Response}, }, - anyhow::Result, model::order::SignedOrderCancellations, - std::{convert::Infallible, sync::Arc}, - warp::{Filter, Rejection}, + std::sync::Arc, }; -pub fn request() -> impl Filter + Clone { - warp::path!("v1" / "orders") - .and(warp::delete()) - .and(extract_payload()) -} - -pub fn response(result: Result<(), OrderCancellationError>) -> super::ApiReply { - convert_json_response(result.map(|_| "Cancelled")) -} - -pub fn filter( - orderbook: Arc, -) -> impl Filter + Clone { - request().and_then(move |cancellations| { - let orderbook = orderbook.clone(); - async move { - let result = orderbook.cancel_orders(cancellations).await; - Result::<_, Infallible>::Ok(response(result)) - } - }) +pub async fn cancel_orders_handler( + State(state): State>, + Json(cancellations): Json, +) -> Response { + match state.orderbook.cancel_orders(cancellations).await { + Ok(_) => (StatusCode::OK, Json("Cancelled")).into_response(), + Err(err) => err.into_response(), + } } diff --git a/crates/orderbook/src/api/get_app_data.rs b/crates/orderbook/src/api/get_app_data.rs index 21c3a099e2..6b4f9f3a96 100644 --- a/crates/orderbook/src/api/get_app_data.rs +++ b/crates/orderbook/src/api/get_app_data.rs @@ -1,42 +1,34 @@ use { - crate::database::Postgres, - anyhow::Result, + crate::api::AppState, app_data::{AppDataDocument, AppDataHash}, - reqwest::StatusCode, - std::convert::Infallible, - warp::{Filter, Rejection, Reply, reply}, + axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, + }, + std::sync::Arc, }; -pub fn request() -> impl Filter + Clone { - warp::path!("v1" / "app_data" / AppDataHash).and(warp::get()) -} - -pub fn get( - database: Postgres, -) -> impl Filter,), Error = Rejection> + Clone { - request().and_then(move |contract_app_data: AppDataHash| { - let database = database.clone(); - async move { - let result = database.get_full_app_data(&contract_app_data).await; - Result::<_, Infallible>::Ok(match result { - Ok(Some(response)) => { - let response = reply::with_status( - reply::json(&AppDataDocument { - full_app_data: response, - }), - StatusCode::OK, - ); - Box::new(response) as Box - } - Ok(None) => Box::new(reply::with_status( - "full app data not found", - StatusCode::NOT_FOUND, - )), - Err(err) => { - tracing::error!(?err, "get_app_data_by_hash"); - Box::new(crate::api::internal_error_reply()) - } - }) +pub async fn get_app_data_handler( + State(state): State>, + Path(contract_app_data): Path, +) -> Response { + let result = state + .database_read + .get_full_app_data(&contract_app_data) + .await; + match result { + Ok(Some(response)) => ( + StatusCode::OK, + Json(AppDataDocument { + full_app_data: response, + }), + ) + .into_response(), + Ok(None) => (StatusCode::NOT_FOUND, "full app data not found").into_response(), + Err(err) => { + tracing::error!(?err, "get_app_data_by_hash"); + crate::api::internal_error_reply() } - }) + } } diff --git a/crates/orderbook/src/api/get_auction.rs b/crates/orderbook/src/api/get_auction.rs index cfe169f8a7..dfca3c1ea9 100644 --- a/crates/orderbook/src/api/get_auction.rs +++ b/crates/orderbook/src/api/get_auction.rs @@ -1,34 +1,25 @@ use { - crate::{api::ApiReply, orderbook::Orderbook}, - anyhow::Result, - reqwest::StatusCode, - std::{convert::Infallible, sync::Arc}, - warp::{Filter, Rejection, reply::with_status}, + crate::api::AppState, + axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json, Response}, + }, + std::sync::Arc, }; -fn get_auction_request() -> impl Filter + Clone { - warp::path!("v1" / "auction").and(warp::get()) -} - -pub fn get_auction( - orderbook: Arc, -) -> impl Filter + Clone { - get_auction_request().and_then(move || { - let orderbook = orderbook.clone(); - async move { - let result = orderbook.get_auction().await; - let reply = match result { - Ok(Some(auction)) => with_status(warp::reply::json(&auction), StatusCode::OK), - Ok(None) => with_status( - super::error("NotFound", "There is no active auction"), - StatusCode::NOT_FOUND, - ), - Err(err) => { - tracing::error!(?err, "/api/v1/get_auction"); - crate::api::internal_error_reply() - } - }; - Result::<_, Infallible>::Ok(reply) +pub async fn get_auction_handler(State(state): State>) -> Response { + let result = state.orderbook.get_auction().await; + match result { + Ok(Some(auction)) => (StatusCode::OK, Json(auction)).into_response(), + Ok(None) => ( + StatusCode::NOT_FOUND, + super::error("NotFound", "There is no active auction"), + ) + .into_response(), + Err(err) => { + tracing::error!(?err, "/api/v1/get_auction"); + crate::api::internal_error_reply() } - }) + } } diff --git a/crates/orderbook/src/api/get_native_price.rs b/crates/orderbook/src/api/get_native_price.rs index b2c027ebce..16e221d327 100644 --- a/crates/orderbook/src/api/get_native_price.rs +++ b/crates/orderbook/src/api/get_native_price.rs @@ -1,50 +1,25 @@ use { - crate::api::{ApiReply, IntoWarpReply}, + crate::api::AppState, alloy::primitives::Address, - anyhow::Result, + axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, + }, model::quote::NativeTokenPrice, - shared::price_estimation::native::NativePriceEstimating, - std::{convert::Infallible, sync::Arc, time::Duration}, - warp::{Filter, Rejection, hyper::StatusCode, reply::with_status}, + std::sync::Arc, }; -fn get_native_prices_request() -> impl Filter + Clone { - warp::path!("v1" / "token" / Address / "native_price").and(warp::get()) -} - -pub fn get_native_price( - estimator: Arc, - quote_timeout: Duration, -) -> impl Filter + Clone { - get_native_prices_request().and_then(move |token: Address| { - let estimator = estimator.clone(); - async move { - let result = estimator.estimate_native_price(token, quote_timeout).await; - let reply = match result { - Ok(price) => with_status( - warp::reply::json(&NativeTokenPrice { price }), - StatusCode::OK, - ), - Err(err) => err.into_warp_reply(), - }; - Result::<_, Infallible>::Ok(reply) - } - }) -} - -#[cfg(test)] -mod tests { - use {super::*, alloy::primitives::address, futures::FutureExt, warp::test::request}; - - #[test] - fn native_prices_query() { - let path = "/v1/token/0xdac17f958d2ee523a2206206994597c13d831ec7/native_price"; - let request = request().path(path).method("GET"); - let result = request - .filter(&get_native_prices_request()) - .now_or_never() - .unwrap() - .unwrap(); - assert_eq!(result, address!("dac17f958d2ee523a2206206994597c13d831ec7")); +pub async fn get_native_price_handler( + State(state): State>, + Path(token): Path
, +) -> Response { + let result = state + .native_price_estimator + .estimate_native_price(token, state.quote_timeout) + .await; + match result { + Ok(price) => (StatusCode::OK, Json(NativeTokenPrice { price })).into_response(), + Err(err) => super::PriceEstimationErrorWrapper(err).into_response(), } } diff --git a/crates/orderbook/src/api/get_order_by_uid.rs b/crates/orderbook/src/api/get_order_by_uid.rs index 9a579eacaf..7cc11721e3 100644 --- a/crates/orderbook/src/api/get_order_by_uid.rs +++ b/crates/orderbook/src/api/get_order_by_uid.rs @@ -1,16 +1,24 @@ use { - crate::orderbook::Orderbook, + crate::api::AppState, anyhow::Result, + axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, + }, model::order::{Order, OrderUid}, - std::{convert::Infallible, sync::Arc}, - warp::{Filter, Rejection, hyper::StatusCode, reply}, + std::sync::Arc, }; -pub fn get_order_by_uid_request() -> impl Filter + Clone { - warp::path!("v1" / "orders" / OrderUid).and(warp::get()) +pub async fn get_order_by_uid_handler( + State(state): State>, + Path(uid): Path, +) -> Response { + let result = state.orderbook.get_order(&uid).await; + get_order_by_uid_response(result) } -pub fn get_order_by_uid_response(result: Result>) -> super::ApiReply { +pub fn get_order_by_uid_response(result: Result>) -> Response { let order = match result { Ok(order) => order, Err(err) => { @@ -19,47 +27,23 @@ pub fn get_order_by_uid_response(result: Result>) -> super::ApiRep } }; match order { - Some(order) => reply::with_status(reply::json(&order), StatusCode::OK), - None => reply::with_status( - super::error("NotFound", "Order was not found"), + Some(order) => (StatusCode::OK, Json(order)).into_response(), + None => ( StatusCode::NOT_FOUND, - ), + super::error("NotFound", "Order was not found"), + ) + .into_response(), } } -pub fn get_order_by_uid( - orderbook: Arc, -) -> impl Filter + Clone { - get_order_by_uid_request().and_then(move |uid| { - let orderbook = orderbook.clone(); - async move { - let result = orderbook.get_order(&uid).await; - Result::<_, Infallible>::Ok(get_order_by_uid_response(result)) - } - }) -} - #[cfg(test)] mod tests { - use { - super::*, - crate::api::response_body, - warp::{Reply, test::request}, - }; - - #[tokio::test] - async fn get_order_by_uid_request_ok() { - let uid = OrderUid::default(); - let request = request().path(&format!("/v1/orders/{uid}")).method("GET"); - let filter = get_order_by_uid_request(); - let result = request.filter(&filter).await.unwrap(); - assert_eq!(result, uid); - } + use {super::*, crate::api::response_body}; #[tokio::test] async fn get_order_by_uid_response_ok() { let order = Order::default(); - let response = get_order_by_uid_response(Ok(Some(order.clone()))).into_response(); + let response = get_order_by_uid_response(Ok(Some(order.clone()))); assert_eq!(response.status(), StatusCode::OK); let body = response_body(response).await; let response_order: Order = serde_json::from_slice(body.as_slice()).unwrap(); @@ -68,7 +52,7 @@ mod tests { #[tokio::test] async fn get_order_by_uid_response_non_existent() { - let response = get_order_by_uid_response(Ok(None)).into_response(); + let response = get_order_by_uid_response(Ok(None)); assert_eq!(response.status(), StatusCode::NOT_FOUND); } } diff --git a/crates/orderbook/src/api/get_order_status.rs b/crates/orderbook/src/api/get_order_status.rs index 90f0d5336e..f9a2fc428f 100644 --- a/crates/orderbook/src/api/get_order_status.rs +++ b/crates/orderbook/src/api/get_order_status.rs @@ -1,36 +1,29 @@ use { - crate::{ - api::ApiReply, - orderbook::{OrderStatusError, Orderbook}, + crate::{api::AppState, orderbook::OrderStatusError}, + axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, }, - anyhow::Result, model::order::OrderUid, - std::{convert::Infallible, sync::Arc}, - warp::{Filter, Rejection, hyper::StatusCode, reply}, + std::sync::Arc, }; -fn get_status_request() -> impl Filter + Clone { - warp::path!("v1" / "orders" / OrderUid / "status").and(warp::get()) -} - -pub fn get_status( - orderbook: Arc, -) -> impl Filter + Clone { - get_status_request().and_then(move |uid| { - let orderbook = orderbook.clone(); - async move { - let status = orderbook.get_order_status(&uid).await; - Result::<_, Infallible>::Ok(match status { - Ok(status) => warp::reply::with_status(warp::reply::json(&status), StatusCode::OK), - Err(OrderStatusError::NotFound) => reply::with_status( - super::error("NotFound", "Order status was not found"), - StatusCode::NOT_FOUND, - ), - Err(err) => { - tracing::error!(?err, "get_order_status"); - *Box::new(crate::api::internal_error_reply()) - } - }) +pub async fn get_status_handler( + State(state): State>, + Path(uid): Path, +) -> Response { + let status = state.orderbook.get_order_status(&uid).await; + match status { + Ok(status) => (StatusCode::OK, Json(status)).into_response(), + Err(OrderStatusError::NotFound) => ( + StatusCode::NOT_FOUND, + super::error("NotFound", "Order status was not found"), + ) + .into_response(), + Err(err) => { + tracing::error!(?err, "get_order_status"); + crate::api::internal_error_reply() } - }) + } } diff --git a/crates/orderbook/src/api/get_orders_by_tx.rs b/crates/orderbook/src/api/get_orders_by_tx.rs index dc357227de..d72f52eb76 100644 --- a/crates/orderbook/src/api/get_orders_by_tx.rs +++ b/crates/orderbook/src/api/get_orders_by_tx.rs @@ -1,47 +1,24 @@ use { - crate::{api::ApiReply, orderbook::Orderbook}, + crate::api::AppState, alloy::primitives::B256, - anyhow::Result, - reqwest::StatusCode, - std::{convert::Infallible, sync::Arc}, - warp::{Filter, Rejection, reply::with_status}, + axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, + }, + std::sync::Arc, }; -pub fn get_orders_by_tx_request() -> impl Filter + Clone { - warp::path!("v1" / "transactions" / B256 / "orders").and(warp::get()) -} - -pub fn get_orders_by_tx( - orderbook: Arc, -) -> impl Filter + Clone { - get_orders_by_tx_request().and_then(move |hash: B256| { - let orderbook = orderbook.clone(); - async move { - let result = orderbook.get_orders_for_tx(&hash).await; - Result::<_, Infallible>::Ok(match result { - Ok(response) => with_status(warp::reply::json(&response), StatusCode::OK), - Err(err) => { - tracing::error!(?err, "get_orders_by_tx"); - crate::api::internal_error_reply() - } - }) +pub async fn get_orders_by_tx_handler( + State(state): State>, + Path(hash): Path, +) -> Response { + let result = state.orderbook.get_orders_for_tx(&hash).await; + match result { + Ok(response) => (StatusCode::OK, Json(response)).into_response(), + Err(err) => { + tracing::error!(?err, "get_orders_by_tx"); + crate::api::internal_error_reply() } - }) -} - -#[cfg(test)] -mod tests { - use {super::*, std::str::FromStr}; - - #[tokio::test] - async fn request_ok() { - let hash_str = "0x0191dbb560e936bd3320d5a505c9c05580a0ebb7e12fe117551ac26e484f295e"; - let result = warp::test::request() - .path(&format!("/v1/transactions/{hash_str}/orders")) - .method("GET") - .filter(&get_orders_by_tx_request()) - .await - .unwrap(); - assert_eq!(result.0, B256::from_str(hash_str).unwrap().0); } } diff --git a/crates/orderbook/src/api/get_solver_competition.rs b/crates/orderbook/src/api/get_solver_competition.rs index 26e7a38390..a0e0ee86da 100644 --- a/crates/orderbook/src/api/get_solver_competition.rs +++ b/crates/orderbook/src/api/get_solver_competition.rs @@ -1,68 +1,54 @@ use { - crate::solver_competition::{Identifier, LoadSolverCompetitionError, SolverCompetitionStoring}, + crate::{ + api::AppState, + solver_competition::{Identifier, LoadSolverCompetitionError, SolverCompetitionStoring}, + }, alloy::primitives::B256, - anyhow::Result, - model::{AuctionId, solver_competition::SolverCompetitionAPI}, - reqwest::StatusCode, - std::{convert::Infallible, sync::Arc}, - warp::{ - Filter, - Rejection, - reply::{Json, WithStatus, with_status}, + axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, }, + model::{AuctionId, solver_competition::SolverCompetitionAPI}, + std::sync::Arc, }; -fn request_id() -> impl Filter + Clone { - warp::path!("v1" / "solver_competition" / AuctionId) - .and(warp::get()) - .map(Identifier::Id) +pub async fn get_solver_competition_by_id_handler( + State(state): State>, + Path(auction_id): Path, +) -> Response { + let handler: &dyn SolverCompetitionStoring = &state.database_read; + let result = handler.load_competition(Identifier::Id(auction_id)).await; + response(result) } -fn request_hash() -> impl Filter + Clone { - warp::path!("v1" / "solver_competition" / "by_tx_hash" / B256) - .and(warp::get()) - .map(Identifier::Transaction) +pub async fn get_solver_competition_by_hash_handler( + State(state): State>, + Path(tx_hash): Path, +) -> Response { + let handler: &dyn SolverCompetitionStoring = &state.database_read; + let result = handler + .load_competition(Identifier::Transaction(tx_hash)) + .await; + response(result) } -fn request_latest() -> impl Filter + Clone { - warp::path!("v1" / "solver_competition" / "latest").and(warp::get()) -} -pub fn get( - handler: Arc, -) -> impl Filter + Clone { - request_id() - .or(request_hash()) - .unify() - .and_then(move |identifier: Identifier| { - let handler = handler.clone(); - async move { - let result = handler.load_competition(identifier).await; - Result::<_, Infallible>::Ok(response(result)) - } - }) -} - -pub fn get_latest( - handler: Arc, -) -> impl Filter + Clone { - request_latest().and_then(move || { - let handler = handler.clone(); - async move { - let result = handler.load_latest_competition().await; - Result::<_, Infallible>::Ok(response(result)) - } - }) +pub async fn get_solver_competition_latest_handler(State(state): State>) -> Response { + let handler: &dyn SolverCompetitionStoring = &state.database_read; + let result = handler.load_latest_competition().await; + response(result) } fn response( result: Result, -) -> WithStatus { +) -> Response { match result { - Ok(response) => with_status(warp::reply::json(&response), StatusCode::OK), - Err(LoadSolverCompetitionError::NotFound) => with_status( - super::error("NotFound", "no competition found"), + Ok(response) => (StatusCode::OK, Json(response)).into_response(), + Err(LoadSolverCompetitionError::NotFound) => ( StatusCode::NOT_FOUND, - ), + super::error("NotFound", "no competition found"), + ) + .into_response(), Err(LoadSolverCompetitionError::Other(err)) => { tracing::error!(?err, "load solver competition"); crate::api::internal_error_reply() @@ -72,43 +58,21 @@ fn response( #[cfg(test)] mod tests { - use { - super::*, - crate::solver_competition::MockSolverCompetitionStoring, - warp::{Reply, test::request}, - }; + use super::*; #[tokio::test] - async fn test() { - let mut storage = MockSolverCompetitionStoring::new(); - storage - .expect_load_competition() - .times(2) - .returning(|_| Ok(Default::default())); - storage - .expect_load_competition() - .times(1) - .return_once(|_| Err(LoadSolverCompetitionError::NotFound)); - let filter = get(Arc::new(storage)); - - let request_ = request().path("/v1/solver_competition/0").method("GET"); - let response = request_.filter(&filter).await.unwrap().into_response(); - dbg!(&response); - assert_eq!(response.status(), StatusCode::OK); - - let request_ = request() - .path( - "/v1/solver_competition/by_tx_hash/\ - 0xd51f28edffcaaa76be4a22f6375ad289272c037f3cc072345676e88d92ced8b5", - ) - .method("GET"); - let response = request_.filter(&filter).await.unwrap().into_response(); - dbg!(&response); - assert_eq!(response.status(), StatusCode::OK); + async fn test_response_ok() { + let result: Result = + Ok(Default::default()); + let resp = response(result); + assert_eq!(resp.status(), StatusCode::OK); + } - let request_ = request().path("/v1/solver_competition/1337").method("GET"); - let response = request_.filter(&filter).await.unwrap().into_response(); - dbg!(&response); - assert_eq!(response.status(), StatusCode::NOT_FOUND); + #[tokio::test] + async fn test_response_not_found() { + let result: Result = + Err(LoadSolverCompetitionError::NotFound); + let resp = response(result); + assert_eq!(resp.status(), StatusCode::NOT_FOUND); } } diff --git a/crates/orderbook/src/api/get_solver_competition_v2.rs b/crates/orderbook/src/api/get_solver_competition_v2.rs index 85850a8ec9..cb9e8bab30 100644 --- a/crates/orderbook/src/api/get_solver_competition_v2.rs +++ b/crates/orderbook/src/api/get_solver_competition_v2.rs @@ -1,73 +1,52 @@ use { - crate::{ - database::Postgres, - solver_competition::{Identifier, LoadSolverCompetitionError}, - }, + crate::{api::AppState, solver_competition::LoadSolverCompetitionError}, alloy::primitives::B256, - anyhow::Result, - model::{AuctionId, solver_competition_v2::Response}, - reqwest::StatusCode, - std::convert::Infallible, - warp::{ - Filter, - Rejection, - reply::{Json, WithStatus, with_status}, + axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, }, + model::{AuctionId, solver_competition_v2}, + std::sync::Arc, }; -fn request_id() -> impl Filter + Clone { - warp::path!("v2" / "solver_competition" / AuctionId) - .and(warp::get()) - .map(Identifier::Id) -} - -fn request_hash() -> impl Filter + Clone { - warp::path!("v2" / "solver_competition" / "by_tx_hash" / B256) - .and(warp::get()) - .map(Identifier::Transaction) -} - -fn request_latest() -> impl Filter + Clone { - warp::path!("v2" / "solver_competition" / "latest").and(warp::get()) +pub async fn get_solver_competition_by_id_handler( + State(state): State>, + Path(auction_id): Path, +) -> Response { + let result = state.database_read.load_competition_by_id(auction_id).await; + response(result) } -pub fn get(db: Postgres) -> impl Filter + Clone { - request_id() - .or(request_hash()) - .unify() - .and_then(move |identifier: Identifier| { - let db = db.clone(); - async move { - let result = match identifier { - Identifier::Id(id) => db.load_competition_by_id(id).await, - Identifier::Transaction(hash) => db.load_competition_by_tx_hash(hash).await, - }; - Result::<_, Infallible>::Ok(response(result)) - } - }) +pub async fn get_solver_competition_by_hash_handler( + State(state): State>, + Path(tx_hash): Path, +) -> Response { + let result = state + .database_read + .load_competition_by_tx_hash(tx_hash) + .await; + response(result) } -pub fn get_latest( - db: Postgres, -) -> impl Filter + Clone { - request_latest().and_then(move || { - let db = db.clone(); - async move { - let result = db.load_latest_competition().await; - Result::<_, Infallible>::Ok(response(result)) - } - }) +pub async fn get_solver_competition_latest_handler(State(state): State>) -> Response { + let result = state.database_read.load_latest_competition().await; + response(result) } fn response( - result: Result, -) -> WithStatus { + result: Result< + solver_competition_v2::Response, + crate::solver_competition::LoadSolverCompetitionError, + >, +) -> Response { match result { - Ok(response) => with_status(warp::reply::json(&response), StatusCode::OK), - Err(LoadSolverCompetitionError::NotFound) => with_status( - super::error("NotFound", "no competition found"), + Ok(response) => (StatusCode::OK, Json(response)).into_response(), + Err(LoadSolverCompetitionError::NotFound) => ( StatusCode::NOT_FOUND, - ), + super::error("NotFound", "no competition found"), + ) + .into_response(), Err(LoadSolverCompetitionError::Other(err)) => { tracing::error!(?err, "load solver competition"); crate::api::internal_error_reply() diff --git a/crates/orderbook/src/api/get_token_metadata.rs b/crates/orderbook/src/api/get_token_metadata.rs index d8f0631564..0607bb6c46 100644 --- a/crates/orderbook/src/api/get_token_metadata.rs +++ b/crates/orderbook/src/api/get_token_metadata.rs @@ -1,31 +1,24 @@ use { - crate::database::Postgres, + crate::api::AppState, alloy::primitives::Address, - hyper::StatusCode, - std::convert::Infallible, - warp::{Filter, Rejection, reply}, + axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, + }, + std::sync::Arc, }; -fn get_native_prices_request() -> impl Filter + Clone { - warp::path!("v1" / "token" / Address / "metadata").and(warp::get()) -} - -pub fn get_token_metadata( - db: Postgres, -) -> impl Filter + Clone { - get_native_prices_request().and_then(move |token: Address| { - let db = db.clone(); - async move { - let result = db.token_metadata(&token).await; - let response = match result { - Ok(metadata) => reply::with_status(reply::json(&metadata), StatusCode::OK), - Err(err) => { - tracing::error!(?err, ?token, "Failed to fetch token's first trade block"); - crate::api::internal_error_reply() - } - }; - - Result::<_, Infallible>::Ok(response) +pub async fn get_token_metadata_handler( + State(state): State>, + Path(token): Path
, +) -> Response { + let result = state.database_read.token_metadata(&token).await; + match result { + Ok(metadata) => (StatusCode::OK, Json(metadata)).into_response(), + Err(err) => { + tracing::error!(?err, ?token, "Failed to fetch token's first trade block"); + crate::api::internal_error_reply() } - }) + } } diff --git a/crates/orderbook/src/api/get_total_surplus.rs b/crates/orderbook/src/api/get_total_surplus.rs index afdbc07b93..04d5434f2f 100644 --- a/crates/orderbook/src/api/get_total_surplus.rs +++ b/crates/orderbook/src/api/get_total_surplus.rs @@ -1,30 +1,31 @@ use { - crate::database::Postgres, + crate::api::AppState, alloy::primitives::Address, + axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, + }, serde_json::json, - std::convert::Infallible, - warp::{Filter, Rejection, http::StatusCode, reply::with_status}, + std::sync::Arc, }; -pub fn get(db: Postgres) -> impl Filter + Clone { - warp::path!("v1" / "users" / Address / "total_surplus") - .and(warp::get()) - .and_then(move |user| { - let db = db.clone(); - async move { - let surplus = db.total_surplus(&user).await; - Result::<_, Infallible>::Ok(match surplus { - Ok(surplus) => with_status( - warp::reply::json(&json!({ - "totalSurplus": surplus.to_string() - })), - StatusCode::OK, - ), - Err(err) => { - tracing::error!(?err, ?user, "failed to compute total surplus"); - crate::api::internal_error_reply() - } - }) - } - }) +pub async fn get_total_surplus_handler( + State(state): State>, + Path(user): Path
, +) -> Response { + let surplus = state.database_read.total_surplus(&user).await; + match surplus { + Ok(surplus) => ( + StatusCode::OK, + Json(json!({ + "totalSurplus": surplus.to_string() + })), + ) + .into_response(), + Err(err) => { + tracing::error!(?err, ?user, "failed to compute total surplus"); + crate::api::internal_error_reply() + } + } } diff --git a/crates/orderbook/src/api/get_trades.rs b/crates/orderbook/src/api/get_trades.rs index a104c7cb55..01652eef19 100644 --- a/crates/orderbook/src/api/get_trades.rs +++ b/crates/orderbook/src/api/get_trades.rs @@ -1,22 +1,23 @@ use { crate::{ - api::{ApiReply, error}, - database::{ - Postgres, - trades::{TradeFilter, TradeRetrieving}, - }, + api::{AppState, error}, + database::trades::{TradeFilter, TradeRetrieving}, }, alloy::primitives::Address, - anyhow::{Context, Result}, + anyhow::Context, + axum::{ + extract::{Query, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, + }, model::order::OrderUid, serde::Deserialize, - std::convert::Infallible, - warp::{Filter, Rejection, hyper::StatusCode, reply::with_status}, + std::sync::Arc, }; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -struct Query { +pub(crate) struct QueryParams { pub order_uid: Option, pub owner: Option
, } @@ -26,7 +27,7 @@ enum TradeFilterError { InvalidFilter(String), } -impl Query { +impl QueryParams { fn trade_filter(&self) -> TradeFilter { TradeFilter { order_uid: self.order_uid, @@ -44,87 +45,71 @@ impl Query { } } -fn get_trades_request() --> impl Filter,), Error = Rejection> + Clone { - warp::path!("v1" / "trades") - .and(warp::get()) - .and(warp::query::()) - .map(|query: Query| query.validate()) -} +pub async fn get_trades_handler( + State(state): State>, + Query(query): Query, +) -> Response { + let trade_filter = match query.validate() { + Ok(trade_filter) => trade_filter, + Err(TradeFilterError::InvalidFilter(msg)) => { + let err = error("InvalidTradeFilter", msg); + return (StatusCode::BAD_REQUEST, err).into_response(); + } + }; -pub fn get_trades(db: Postgres) -> impl Filter + Clone { - get_trades_request().and_then(move |request_result| { - let database = db.clone(); - async move { - Result::<_, Infallible>::Ok(match request_result { - Ok(trade_filter) => { - let result = database.trades(&trade_filter).await.context("get_trades"); - match result { - Ok(reply) => with_status(warp::reply::json(&reply), StatusCode::OK), - Err(err) => { - tracing::error!(?err, "get_trades"); - crate::api::internal_error_reply() - } - } - } - Err(TradeFilterError::InvalidFilter(msg)) => { - let err = error("InvalidTradeFilter", msg); - with_status(err, StatusCode::BAD_REQUEST) - } - }) + let result = state + .database_read + .trades(&trade_filter) + .await + .context("get_trades"); + match result { + Ok(reply) => (StatusCode::OK, Json(reply)).into_response(), + Err(err) => { + tracing::error!(?err, "get_trades"); + crate::api::internal_error_reply() } - }) + } } #[cfg(test)] mod tests { - use { - super::*, - warp::test::{RequestBuilder, request}, - }; - - #[tokio::test] - async fn get_trades_request_ok() { - let trade_filter = |request: RequestBuilder| async move { - let filter = get_trades_request(); - request.method("GET").filter(&filter).await - }; + use {super::*, alloy::primitives::Address, model::order::OrderUid}; + #[test] + fn query_validation_ok() { let owner = Address::with_last_byte(1); - let owner_path = format!("/v1/trades?owner=0x{owner:x}"); - let result = trade_filter(request().path(owner_path.as_str())) - .await - .unwrap() - .unwrap(); + let query = QueryParams { + owner: Some(owner), + order_uid: None, + }; + let result = query.validate().unwrap(); assert_eq!(result.owner, Some(owner)); assert_eq!(result.order_uid, None); let uid = OrderUid([1u8; 56]); - let order_uid_path = format!("/v1/trades?orderUid={uid}"); - let result = trade_filter(request().path(order_uid_path.as_str())) - .await - .unwrap() - .unwrap(); + let query = QueryParams { + owner: None, + order_uid: Some(uid), + }; + let result = query.validate().unwrap(); assert_eq!(result.owner, None); assert_eq!(result.order_uid, Some(uid)); } - #[tokio::test] - async fn get_trades_request_err() { - let trade_filter = |request: RequestBuilder| async move { - let filter = get_trades_request(); - request.method("GET").filter(&filter).await - }; - + #[test] + fn query_validation_err() { let owner = Address::with_last_byte(1); let uid = OrderUid([1u8; 56]); - let path = format!("/v1/trades?owner=0x{owner:x}&orderUid={uid}"); - - let result = trade_filter(request().path(path.as_str())).await.unwrap(); - assert!(result.is_err()); + let query = QueryParams { + owner: Some(owner), + order_uid: Some(uid), + }; + assert!(query.validate().is_err()); - let path = "/v1/trades"; - let result = trade_filter(request().path(path)).await.unwrap(); - assert!(result.is_err()); + let query = QueryParams { + owner: None, + order_uid: None, + }; + assert!(query.validate().is_err()); } } diff --git a/crates/orderbook/src/api/get_trades_v2.rs b/crates/orderbook/src/api/get_trades_v2.rs index a570ccf3a5..b3c1073849 100644 --- a/crates/orderbook/src/api/get_trades_v2.rs +++ b/crates/orderbook/src/api/get_trades_v2.rs @@ -1,22 +1,23 @@ use { crate::{ - api::{ApiReply, error}, - database::{ - Postgres, - trades::{PaginatedTradeFilter, TradeRetrievingPaginated}, - }, + api::{AppState, error}, + database::trades::{PaginatedTradeFilter, TradeRetrievingPaginated}, }, alloy::primitives::Address, - anyhow::{Context, Result}, + anyhow::Context, + axum::{ + extract::{Query, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, + }, model::order::OrderUid, serde::Deserialize, - std::convert::Infallible, - warp::{Filter, Rejection, hyper::StatusCode, reply::with_status}, + std::sync::Arc, }; #[derive(Deserialize)] #[serde(rename_all = "camelCase")] -struct Query { +pub(crate) struct QueryParams { pub order_uid: Option, pub owner: Option
, pub offset: Option, @@ -34,7 +35,7 @@ enum TradeFilterError { InvalidLimit(u64, u64), } -impl Query { +impl QueryParams { fn trade_filter(&self, offset: u64, limit: u64) -> PaginatedTradeFilter { PaginatedTradeFilter { order_uid: self.order_uid, @@ -63,126 +64,124 @@ impl Query { } } -fn get_trades_request() --> impl Filter,), Error = Rejection> + Clone -{ - warp::path!("v2" / "trades") - .and(warp::get()) - .and(warp::query::()) - .map(|query: Query| query.validate()) -} +pub async fn get_trades_handler( + State(state): State>, + Query(query): Query, +) -> Response { + let trade_filter = match query.validate() { + Ok(trade_filter) => trade_filter, + Err(TradeFilterError::InvalidFilter(msg)) => { + let err = error("InvalidTradeFilter", msg); + return (StatusCode::BAD_REQUEST, err).into_response(); + } + Err(TradeFilterError::InvalidLimit(min, max)) => { + let err = error( + "InvalidLimit", + format!("limit must be between {min} and {max}"), + ); + return (StatusCode::BAD_REQUEST, err).into_response(); + } + }; -pub fn get_trades(db: Postgres) -> impl Filter + Clone { - get_trades_request().and_then(move |request_result| { - let database = db.clone(); - async move { - Result::<_, Infallible>::Ok(match request_result { - Ok(trade_filter) => { - let result = database - .trades_paginated(&trade_filter) - .await - .context("get_trades_v2"); - match result { - Ok(reply) => with_status(warp::reply::json(&reply), StatusCode::OK), - Err(err) => { - tracing::error!(?err, "get_trades_v2"); - crate::api::internal_error_reply() - } - } - } - Err(TradeFilterError::InvalidFilter(msg)) => { - let err = error("InvalidTradeFilter", msg); - with_status(err, StatusCode::BAD_REQUEST) - } - Err(TradeFilterError::InvalidLimit(min, max)) => { - let err = error( - "InvalidLimit", - format!("limit must be between {min} and {max}"), - ); - with_status(err, StatusCode::BAD_REQUEST) - } - }) + let result = state + .database_read + .trades_paginated(&trade_filter) + .await + .context("get_trades_v2"); + match result { + Ok(reply) => (StatusCode::OK, Json(reply)).into_response(), + Err(err) => { + tracing::error!(?err, "get_trades_v2"); + crate::api::internal_error_reply() } - }) + } } #[cfg(test)] mod tests { - use { - super::*, - warp::test::{RequestBuilder, request}, - }; - - #[tokio::test] - async fn get_trades_request_ok() { - let trade_filter = |request: RequestBuilder| async move { - let filter = get_trades_request(); - request.method("GET").filter(&filter).await - }; + use {super::*, alloy::primitives::Address, model::order::OrderUid}; + #[test] + fn query_validation_ok() { let owner = Address::with_last_byte(1); - let owner_path = format!("/v2/trades?owner=0x{owner:x}"); - let result = trade_filter(request().path(owner_path.as_str())) - .await - .unwrap() - .unwrap(); + let query = QueryParams { + owner: Some(owner), + order_uid: None, + offset: None, + limit: None, + }; + let result = query.validate().unwrap(); assert_eq!(result.owner, Some(owner)); assert_eq!(result.order_uid, None); assert_eq!(result.offset, DEFAULT_OFFSET); assert_eq!(result.limit, DEFAULT_LIMIT); let uid = OrderUid([1u8; 56]); - let order_uid_path = format!("/v2/trades?orderUid={uid}"); - let result = trade_filter(request().path(order_uid_path.as_str())) - .await - .unwrap() - .unwrap(); + let query = QueryParams { + owner: None, + order_uid: Some(uid), + offset: None, + limit: None, + }; + let result = query.validate().unwrap(); assert_eq!(result.owner, None); assert_eq!(result.order_uid, Some(uid)); assert_eq!(result.offset, DEFAULT_OFFSET); assert_eq!(result.limit, DEFAULT_LIMIT); // Test with custom offset and limit - let owner_path = format!("/v2/trades?owner=0x{owner:x}&offset=10&limit=50"); - let result = trade_filter(request().path(owner_path.as_str())) - .await - .unwrap() - .unwrap(); + let query = QueryParams { + owner: Some(owner), + order_uid: None, + offset: Some(10), + limit: Some(50), + }; + let result = query.validate().unwrap(); assert_eq!(result.owner, Some(owner)); assert_eq!(result.offset, 10); assert_eq!(result.limit, 50); } - #[tokio::test] - async fn get_trades_request_err() { - let trade_filter = |request: RequestBuilder| async move { - let filter = get_trades_request(); - request.method("GET").filter(&filter).await - }; - + #[test] + fn query_validation_err() { let owner = Address::with_last_byte(1); let uid = OrderUid([1u8; 56]); - let path = format!("/v2/trades?owner=0x{owner:x}&orderUid={uid}"); - - let result = trade_filter(request().path(path.as_str())).await.unwrap(); - assert!(result.is_err()); + let query = QueryParams { + owner: Some(owner), + order_uid: Some(uid), + offset: None, + limit: None, + }; + assert!(query.validate().is_err()); - let path = "/v2/trades"; - let result = trade_filter(request().path(path)).await.unwrap(); - assert!(result.is_err()); + let query = QueryParams { + owner: None, + order_uid: None, + offset: None, + limit: None, + }; + assert!(query.validate().is_err()); // Test limit validation - let path = format!("/v2/trades?owner=0x{owner:x}&limit=0"); - let result = trade_filter(request().path(path.as_str())).await.unwrap(); + let query = QueryParams { + owner: Some(owner), + order_uid: None, + offset: None, + limit: Some(0), + }; assert!(matches!( - result, + query.validate(), Err(TradeFilterError::InvalidLimit(MIN_LIMIT, MAX_LIMIT)) )); - let path = format!("/v2/trades?owner=0x{owner:x}&limit=1001"); - let result = trade_filter(request().path(path.as_str())).await.unwrap(); + let query = QueryParams { + owner: Some(owner), + order_uid: None, + offset: None, + limit: Some(1001), + }; assert!(matches!( - result, + query.validate(), Err(TradeFilterError::InvalidLimit(MIN_LIMIT, MAX_LIMIT)) )); } diff --git a/crates/orderbook/src/api/get_user_orders.rs b/crates/orderbook/src/api/get_user_orders.rs index 77783b29bf..8330a72313 100644 --- a/crates/orderbook/src/api/get_user_orders.rs +++ b/crates/orderbook/src/api/get_user_orders.rs @@ -1,82 +1,51 @@ use { - crate::{api::ApiReply, orderbook::Orderbook}, + crate::api::AppState, alloy::primitives::Address, - anyhow::Result, + axum::{ + extract::{Path, Query, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, + }, serde::Deserialize, - std::{convert::Infallible, sync::Arc}, - warp::{Filter, Rejection, hyper::StatusCode, reply::with_status}, + std::sync::Arc, }; #[derive(Clone, Copy, Debug, Deserialize)] -struct Query { +pub(crate) struct QueryParams { offset: Option, limit: Option, } -fn request() -> impl Filter + Clone { - warp::path!("v1" / "account" / Address / "orders") - .and(warp::get()) - .and(warp::query::()) -} - -pub fn get_user_orders( - orderbook: Arc, -) -> impl Filter + Clone { - request().and_then(move |owner: Address, query: Query| { - let orderbook = orderbook.clone(); - async move { - const DEFAULT_OFFSET: u64 = 0; - const DEFAULT_LIMIT: u64 = 10; - const MIN_LIMIT: u64 = 1; - const MAX_LIMIT: u64 = 1000; - let offset = query.offset.unwrap_or(DEFAULT_OFFSET); - let limit = query.limit.unwrap_or(DEFAULT_LIMIT); - if !(MIN_LIMIT..=MAX_LIMIT).contains(&limit) { - return Ok(with_status( - super::error( - "LIMIT_OUT_OF_BOUNDS", - format!("The pagination limit is [{MIN_LIMIT},{MAX_LIMIT}]."), - ), - StatusCode::BAD_REQUEST, - )); - } - let result = orderbook.get_user_orders(&owner, offset, limit).await; - Result::<_, Infallible>::Ok(match result { - Ok(reply) => with_status(warp::reply::json(&reply), StatusCode::OK), - Err(err) => { - tracing::error!(?err, "get_user_orders"); - crate::api::internal_error_reply() - } - }) - } - }) -} +pub async fn get_user_orders_handler( + State(state): State>, + Path(owner): Path
, + Query(query): Query, +) -> Response { + const DEFAULT_OFFSET: u64 = 0; + const DEFAULT_LIMIT: u64 = 10; + const MIN_LIMIT: u64 = 1; + const MAX_LIMIT: u64 = 1000; -#[cfg(test)] -mod tests { - use super::*; + let offset = query.offset.unwrap_or(DEFAULT_OFFSET); + let limit = query.limit.unwrap_or(DEFAULT_LIMIT); - #[tokio::test] - async fn request_() { - let path = "/v1/account/0x0000000000000000000000000000000000000001/orders"; - let result = warp::test::request() - .path(path) - .method("GET") - .filter(&request()) - .await - .unwrap(); - assert_eq!(result.0, Address::with_last_byte(1)); - assert_eq!(result.1.offset, None); - assert_eq!(result.1.limit, None); + if !(MIN_LIMIT..=MAX_LIMIT).contains(&limit) { + return ( + StatusCode::BAD_REQUEST, + super::error( + "LIMIT_OUT_OF_BOUNDS", + format!("The pagination limit is [{MIN_LIMIT},{MAX_LIMIT}]."), + ), + ) + .into_response(); + } - let path = "/v1/account/0x0000000000000000000000000000000000000001/orders?offset=1&limit=2"; - let result = warp::test::request() - .path(path) - .method("GET") - .filter(&request()) - .await - .unwrap(); - assert_eq!(result.1.offset, Some(1)); - assert_eq!(result.1.limit, Some(2)); + let result = state.orderbook.get_user_orders(&owner, offset, limit).await; + match result { + Ok(reply) => (StatusCode::OK, Json(reply)).into_response(), + Err(err) => { + tracing::error!(?err, "get_user_orders"); + crate::api::internal_error_reply() + } } } diff --git a/crates/orderbook/src/api/post_order.rs b/crates/orderbook/src/api/post_order.rs index 7a57ac4ed4..3c38c70fe0 100644 --- a/crates/orderbook/src/api/post_order.rs +++ b/crates/orderbook/src/api/post_order.rs @@ -1,9 +1,15 @@ use { crate::{ - api::{ApiReply, IntoWarpReply, error, extract_payload}, - orderbook::{AddOrderError, OrderReplacementError, Orderbook}, + api::{AppState, error}, + orderbook::{AddOrderError, OrderReplacementError}, }, anyhow::Result, + axum::{ + Json, + extract::State, + http::StatusCode, + response::{IntoResponse, Response}, + }, model::{ order::{AppdataFromMismatch, OrderCreation, OrderUid}, quote::QuoteId, @@ -15,77 +21,73 @@ use { PartialValidationError, ValidationError, }, - std::{convert::Infallible, sync::Arc}, - warp::{ - Filter, - Rejection, - hyper::StatusCode, - reply::{self, with_status}, - }, + std::sync::Arc, }; -pub fn create_order_request() -> impl Filter + Clone -{ - warp::path!("v1" / "orders") - .and(warp::post()) - .and(extract_payload()) -} - pub struct PartialValidationErrorWrapper(pub PartialValidationError); -impl IntoWarpReply for PartialValidationErrorWrapper { - fn into_warp_reply(self) -> ApiReply { +impl IntoResponse for PartialValidationErrorWrapper { + fn into_response(self) -> Response { match self.0 { - PartialValidationError::UnsupportedBuyTokenDestination(dest) => with_status( + PartialValidationError::UnsupportedBuyTokenDestination(dest) => ( + StatusCode::BAD_REQUEST, error("UnsupportedBuyTokenDestination", format!("Type {dest:?}")), + ) + .into_response(), + PartialValidationError::UnsupportedSellTokenSource(src) => ( StatusCode::BAD_REQUEST, - ), - PartialValidationError::UnsupportedSellTokenSource(src) => with_status( error("UnsupportedSellTokenSource", format!("Type {src:?}")), + ) + .into_response(), + PartialValidationError::UnsupportedOrderType => ( StatusCode::BAD_REQUEST, - ), - PartialValidationError::UnsupportedOrderType => with_status( error( "UnsupportedOrderType", "This order type is currently not supported", ), - StatusCode::BAD_REQUEST, - ), - PartialValidationError::Forbidden => with_status( - error("Forbidden", "Forbidden, your account is deny-listed"), + ) + .into_response(), + PartialValidationError::Forbidden => ( StatusCode::FORBIDDEN, - ), - PartialValidationError::ValidTo(OrderValidToError::Insufficient) => with_status( + error("Forbidden", "Forbidden, your account is deny-listed"), + ) + .into_response(), + PartialValidationError::ValidTo(OrderValidToError::Insufficient) => ( + StatusCode::BAD_REQUEST, error( "InsufficientValidTo", "validTo is not far enough in the future", ), + ) + .into_response(), + PartialValidationError::ValidTo(OrderValidToError::Excessive) => ( StatusCode::BAD_REQUEST, - ), - PartialValidationError::ValidTo(OrderValidToError::Excessive) => with_status( error("ExcessiveValidTo", "validTo is too far into the future"), + ) + .into_response(), + PartialValidationError::InvalidNativeSellToken => ( StatusCode::BAD_REQUEST, - ), - PartialValidationError::InvalidNativeSellToken => with_status( error( "InvalidNativeSellToken", "The chain's native token (Ether/xDai) cannot be used as the sell token", ), + ) + .into_response(), + PartialValidationError::SameBuyAndSellToken => ( StatusCode::BAD_REQUEST, - ), - PartialValidationError::SameBuyAndSellToken => with_status( error( "SameBuyAndSellToken", "Buy token is the same as the sell token.", ), + ) + .into_response(), + PartialValidationError::UnsupportedToken { token, reason } => ( StatusCode::BAD_REQUEST, - ), - PartialValidationError::UnsupportedToken { token, reason } => with_status( error( "UnsupportedToken", format!("Token {token:?} is unsupported: {reason}"), ), - StatusCode::BAD_REQUEST, - ), + ) + .into_response(), PartialValidationError::Other(err) => { tracing::error!(?err, "PartialValidatonError"); crate::api::internal_error_reply() @@ -95,14 +97,16 @@ impl IntoWarpReply for PartialValidationErrorWrapper { } pub struct AppDataValidationErrorWrapper(pub AppDataValidationError); -impl IntoWarpReply for AppDataValidationErrorWrapper { - fn into_warp_reply(self) -> ApiReply { +impl IntoResponse for AppDataValidationErrorWrapper { + fn into_response(self) -> Response { match self.0 { - AppDataValidationError::Invalid(err) => with_status( + AppDataValidationError::Invalid(err) => ( + StatusCode::BAD_REQUEST, error("InvalidAppData", format!("{err:?}")), + ) + .into_response(), + AppDataValidationError::Mismatch { provided, actual } => ( StatusCode::BAD_REQUEST, - ), - AppDataValidationError::Mismatch { provided, actual } => with_status( error( "AppDataHashMismatch", format!( @@ -110,30 +114,34 @@ impl IntoWarpReply for AppDataValidationErrorWrapper { {provided:?}", ), ), - StatusCode::BAD_REQUEST, - ), + ) + .into_response(), } } } pub struct ValidationErrorWrapper(ValidationError); -impl IntoWarpReply for ValidationErrorWrapper { - fn into_warp_reply(self) -> ApiReply { +impl IntoResponse for ValidationErrorWrapper { + fn into_response(self) -> Response { match self.0 { - ValidationError::Partial(pre) => PartialValidationErrorWrapper(pre).into_warp_reply(), - ValidationError::AppData(err) => AppDataValidationErrorWrapper(err).into_warp_reply(), - ValidationError::PriceForQuote(err) => err.into_warp_reply(), - ValidationError::MissingFrom => with_status( + ValidationError::Partial(pre) => PartialValidationErrorWrapper(pre).into_response(), + ValidationError::AppData(err) => AppDataValidationErrorWrapper(err).into_response(), + ValidationError::PriceForQuote(err) => { + super::PriceEstimationErrorWrapper(err).into_response() + } + ValidationError::MissingFrom => ( + StatusCode::BAD_REQUEST, error( "MissingFrom", "From address must be specified for on-chain signature", ), - StatusCode::BAD_REQUEST, - ), + ) + .into_response(), ValidationError::AppdataFromMismatch(AppdataFromMismatch { from, app_data_signer, - }) => with_status( + }) => ( + StatusCode::BAD_REQUEST, error( "AppdataFromMismatch", format!( @@ -141,9 +149,10 @@ impl IntoWarpReply for ValidationErrorWrapper { {app_data_signer:?} specified in the app data" ), ), + ) + .into_response(), + ValidationError::WrongOwner(signature::Recovered { message, signer }) => ( StatusCode::BAD_REQUEST, - ), - ValidationError::WrongOwner(signature::Recovered { message, signer }) => with_status( error( "WrongOwner", format!( @@ -151,78 +160,90 @@ impl IntoWarpReply for ValidationErrorWrapper { from address" ), ), + ) + .into_response(), + ValidationError::InvalidEip1271Signature(hash) => ( StatusCode::BAD_REQUEST, - ), - ValidationError::InvalidEip1271Signature(hash) => with_status( error( "InvalidEip1271Signature", format!("signature for computed order hash {hash:?} is not valid"), ), + ) + .into_response(), + ValidationError::InsufficientBalance => ( StatusCode::BAD_REQUEST, - ), - ValidationError::InsufficientBalance => with_status( error( "InsufficientBalance", "order owner must have funds worth at least x in his account", ), + ) + .into_response(), + ValidationError::InsufficientAllowance => ( StatusCode::BAD_REQUEST, - ), - ValidationError::InsufficientAllowance => with_status( error( "InsufficientAllowance", "order owner must give allowance to VaultRelayer", ), + ) + .into_response(), + ValidationError::InvalidSignature => ( StatusCode::BAD_REQUEST, - ), - ValidationError::InvalidSignature => with_status( error("InvalidSignature", "invalid signature"), + ) + .into_response(), + ValidationError::NonZeroFee => ( StatusCode::BAD_REQUEST, - ), - ValidationError::NonZeroFee => with_status( error("NonZeroFee", "Fee must be zero"), - StatusCode::BAD_REQUEST, - ), - ValidationError::SellAmountOverflow => with_status( + ) + .into_response(), + ValidationError::SellAmountOverflow => ( + StatusCode::INTERNAL_SERVER_ERROR, error( "SellAmountOverflow", "Sell amount + fee amount must fit in U256", ), - StatusCode::INTERNAL_SERVER_ERROR, - ), - ValidationError::TransferSimulationFailed => with_status( + ) + .into_response(), + ValidationError::TransferSimulationFailed => ( + StatusCode::BAD_REQUEST, error( "TransferSimulationFailed", "sell token cannot be transferred", ), + ) + .into_response(), + ValidationError::QuoteNotVerified => ( StatusCode::BAD_REQUEST, - ), - ValidationError::QuoteNotVerified => with_status( error( "QuoteNotVerified", "No quote for this trade could be verified to be accurate. Aborting the order \ creation since it will likely not be executed.", ), + ) + .into_response(), + ValidationError::ZeroAmount => ( StatusCode::BAD_REQUEST, - ), - ValidationError::ZeroAmount => with_status( error("ZeroAmount", "Buy or sell amount is zero."), + ) + .into_response(), + ValidationError::IncompatibleSigningScheme => ( StatusCode::BAD_REQUEST, - ), - ValidationError::IncompatibleSigningScheme => with_status( error( "IncompatibleSigningScheme", "Signing scheme is not compatible with order placement method.", ), + ) + .into_response(), + ValidationError::TooManyLimitOrders => ( StatusCode::BAD_REQUEST, - ), - ValidationError::TooManyLimitOrders => with_status( error("TooManyLimitOrders", "Too many limit orders"), + ) + .into_response(), + ValidationError::TooMuchGas => ( StatusCode::BAD_REQUEST, - ), - ValidationError::TooMuchGas => with_status( error("TooMuchGas", "Executing order requires too many gas units"), - StatusCode::BAD_REQUEST, - ), + ) + .into_response(), ValidationError::Other(err) => { tracing::error!(?err, "ValidationErrorWrapper"); @@ -232,14 +253,15 @@ impl IntoWarpReply for ValidationErrorWrapper { } } -impl IntoWarpReply for AddOrderError { - fn into_warp_reply(self) -> ApiReply { +impl IntoResponse for AddOrderError { + fn into_response(self) -> Response { match self { - Self::OrderValidation(err) => ValidationErrorWrapper(err).into_warp_reply(), - Self::DuplicatedOrder => with_status( - error("DuplicatedOrder", "order already exists"), + Self::OrderValidation(err) => ValidationErrorWrapper(err).into_response(), + Self::DuplicatedOrder => ( StatusCode::BAD_REQUEST, - ), + error("DuplicatedOrder", "order already exists"), + ) + .into_response(), Self::Database(err) => { tracing::error!(?err, "AddOrderError"); crate::api::internal_error_reply() @@ -253,38 +275,43 @@ impl IntoWarpReply for AddOrderError { ); crate::api::internal_error_reply() } - AddOrderError::OrderNotFound(err) => err.into_warp_reply(), - AddOrderError::InvalidAppData(err) => reply::with_status( - super::error("InvalidAppData", err.to_string()), + AddOrderError::OrderNotFound(err) => err.into_response(), + AddOrderError::InvalidAppData(err) => ( StatusCode::BAD_REQUEST, - ), - AddOrderError::InvalidReplacement(err) => err.into_warp_reply(), - AddOrderError::MetadataSerializationFailed(err) => reply::with_status( - super::error("MetadataSerializationFailed", err.to_string()), + super::error("InvalidAppData", err.to_string()), + ) + .into_response(), + AddOrderError::InvalidReplacement(err) => err.into_response(), + AddOrderError::MetadataSerializationFailed(err) => ( StatusCode::INTERNAL_SERVER_ERROR, - ), + super::error("MetadataSerializationFailed", err.to_string()), + ) + .into_response(), } } } -impl IntoWarpReply for OrderReplacementError { - fn into_warp_reply(self) -> super::ApiReply { +impl IntoResponse for OrderReplacementError { + fn into_response(self) -> Response { match self { - OrderReplacementError::InvalidSignature => with_status( - super::error("InvalidSignature", "Malformed signature"), + OrderReplacementError::InvalidSignature => ( StatusCode::BAD_REQUEST, - ), - OrderReplacementError::WrongOwner => with_status( - super::error("WrongOwner", "Old and new orders have different signers"), + super::error("InvalidSignature", "Malformed signature"), + ) + .into_response(), + OrderReplacementError::WrongOwner => ( StatusCode::UNAUTHORIZED, - ), - OrderReplacementError::OldOrderActivelyBidOn => with_status( + super::error("WrongOwner", "Old and new orders have different signers"), + ) + .into_response(), + OrderReplacementError::OldOrderActivelyBidOn => ( + StatusCode::BAD_REQUEST, super::error( "OldOrderActivelyBidOn", "The old order is actively beign bid on in recent auctions", ), - StatusCode::BAD_REQUEST, - ), + ) + .into_response(), OrderReplacementError::Other(err) => { tracing::error!(?err, "replace_order"); crate::api::internal_error_reply() @@ -295,68 +322,46 @@ impl IntoWarpReply for OrderReplacementError { pub fn create_order_response( result: Result<(OrderUid, Option), AddOrderError>, -) -> ApiReply { +) -> Response { match result { - Ok((uid, _)) => with_status(warp::reply::json(&uid), StatusCode::CREATED), - Err(err) => err.into_warp_reply(), + Ok((uid, _)) => (StatusCode::CREATED, Json(uid)).into_response(), + Err(err) => err.into_response(), } } -pub fn post_order( - orderbook: Arc, -) -> impl Filter + Clone { - create_order_request().and_then(move |order: OrderCreation| { - let orderbook = orderbook.clone(); - async move { - let result = orderbook - .add_order(order.clone()) - .await - .map(|(order_uid, quote_metadata)| { - let quote_id = quote_metadata.as_ref().and_then(|q| q.id); - let quote_solver = quote_metadata.as_ref().map(|q| q.solver); - tracing::debug!(%order_uid, ?quote_id, ?quote_solver, "order created"); - (order_uid, quote_metadata.and_then(|quote| quote.id)) - }) - .inspect_err(|err| { - tracing::debug!(?order, ?err, "error creating order"); - }); +pub async fn post_order_handler( + State(state): State>, + Json(order): Json, +) -> Response { + let result = state + .orderbook + .add_order(order.clone()) + .await + .map(|(order_uid, quote_metadata)| { + let quote_id = quote_metadata.as_ref().and_then(|q| q.id); + let quote_solver = quote_metadata.as_ref().map(|q| q.solver); + tracing::debug!(%order_uid, ?quote_id, ?quote_solver, "order created"); + (order_uid, quote_metadata.and_then(|quote| quote.id)) + }) + .inspect_err(|err| { + tracing::debug!(?order, ?err, "error creating order"); + }); - Result::<_, Infallible>::Ok(create_order_response(result)) - } - }) + create_order_response(result) } #[cfg(test)] mod tests { - use { - super::*, - crate::api::response_body, - model::order::{OrderCreation, OrderUid}, - serde_json::json, - warp::{Reply, test::request}, - }; - - #[tokio::test] - async fn create_order_request_ok() { - let filter = create_order_request(); - let order_payload = OrderCreation::default(); - let request = request() - .path("/v1/orders") - .method("POST") - .header("content-type", "application/json") - .json(&order_payload); - let result = request.filter(&filter).await.unwrap(); - assert_eq!(result, order_payload); - } + use {super::*, crate::api::response_body, model::order::OrderUid, serde_json::json}; #[tokio::test] async fn create_order_response_created() { let uid = OrderUid([1u8; 56]); - let response = create_order_response(Ok((uid, Some(42)))).into_response(); + let response = create_order_response(Ok((uid, Some(42)))); assert_eq!(response.status(), StatusCode::CREATED); let body = response_body(response).await; let body: serde_json::Value = serde_json::from_slice(body.as_slice()).unwrap(); - let expected= json!( + let expected = json!( "0x0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101" ); assert_eq!(body, expected); @@ -364,7 +369,7 @@ mod tests { #[tokio::test] async fn create_order_response_duplicate() { - let response = create_order_response(Err(AddOrderError::DuplicatedOrder)).into_response(); + let response = create_order_response(Err(AddOrderError::DuplicatedOrder)); assert_eq!(response.status(), StatusCode::BAD_REQUEST); let body = response_body(response).await; let body: serde_json::Value = serde_json::from_slice(body.as_slice()).unwrap(); diff --git a/crates/orderbook/src/api/post_quote.rs b/crates/orderbook/src/api/post_quote.rs index 57cc81c680..0244e0a074 100644 --- a/crates/orderbook/src/api/post_quote.rs +++ b/crates/orderbook/src/api/post_quote.rs @@ -1,80 +1,73 @@ use { super::post_order::{AppDataValidationErrorWrapper, PartialValidationErrorWrapper}, crate::{ - api::{self, ApiReply, IntoWarpReply, convert_json_response, error, rich_error}, - quoter::{OrderQuoteError, QuoteHandler}, + api::{AppState, error, rich_error}, + quoter::OrderQuoteError, + }, + axum::{ + Json, + extract::State, + response::{IntoResponse, Response}, }, - anyhow::Result, model::quote::OrderQuoteRequest, reqwest::StatusCode, shared::order_quoting::CalculateQuoteError, - std::{convert::Infallible, sync::Arc}, + std::sync::Arc, thiserror::Error, - warp::{Filter, Rejection}, }; -fn post_quote_request() -> impl Filter + Clone { - warp::path!("v1" / "quote") - .and(warp::post()) - .and(api::extract_payload()) -} - -pub fn post_quote( - quotes: Arc, -) -> impl Filter + Clone { - post_quote_request().and_then(move |request: OrderQuoteRequest| { - let quotes = quotes.clone(); - async move { - let result = quotes - .calculate_quote(&request) - .await - .map_err(OrderQuoteErrorWrapper); - if let Err(err) = &result { - tracing::warn!(%err, ?request, "post_quote error"); - } - Result::<_, Infallible>::Ok(convert_json_response(result)) +pub async fn post_quote_handler( + State(state): State>, + Json(request): Json, +) -> Response { + let result = state.quotes.calculate_quote(&request).await; + match result { + Ok(response) => (StatusCode::OK, Json(response)).into_response(), + Err(err) => { + tracing::warn!(%err, ?request, "post_quote error"); + OrderQuoteErrorWrapper(err).into_response() } - }) + } } #[derive(Debug, Error)] #[error(transparent)] pub struct OrderQuoteErrorWrapper(pub OrderQuoteError); -impl IntoWarpReply for OrderQuoteErrorWrapper { - fn into_warp_reply(self) -> ApiReply { +impl IntoResponse for OrderQuoteErrorWrapper { + fn into_response(self) -> Response { match self.0 { - OrderQuoteError::AppData(err) => AppDataValidationErrorWrapper(err).into_warp_reply(), - OrderQuoteError::Order(err) => PartialValidationErrorWrapper(err).into_warp_reply(), - OrderQuoteError::CalculateQuote(err) => { - CalculateQuoteErrorWrapper(err).into_warp_reply() - } + OrderQuoteError::AppData(err) => AppDataValidationErrorWrapper(err).into_response(), + OrderQuoteError::Order(err) => PartialValidationErrorWrapper(err).into_response(), + OrderQuoteError::CalculateQuote(err) => CalculateQuoteErrorWrapper(err).into_response(), } } } pub struct CalculateQuoteErrorWrapper(CalculateQuoteError); -impl IntoWarpReply for CalculateQuoteErrorWrapper { - fn into_warp_reply(self) -> ApiReply { +impl IntoResponse for CalculateQuoteErrorWrapper { + fn into_response(self) -> Response { match self.0 { - CalculateQuoteError::Price { source, .. } => source.into_warp_reply(), - CalculateQuoteError::SellAmountDoesNotCoverFee { fee_amount } => { - warp::reply::with_status( - rich_error( - "SellAmountDoesNotCoverFee", - "The sell amount for the sell order is lower than the fee.", - serde_json::json!({ "fee_amount": fee_amount }), - ), - StatusCode::BAD_REQUEST, - ) + CalculateQuoteError::Price { source, .. } => { + super::PriceEstimationErrorWrapper(source).into_response() } - CalculateQuoteError::QuoteNotVerified => warp::reply::with_status( + CalculateQuoteError::SellAmountDoesNotCoverFee { fee_amount } => ( + StatusCode::BAD_REQUEST, + rich_error( + "SellAmountDoesNotCoverFee", + "The sell amount for the sell order is lower than the fee.", + serde_json::json!({ "fee_amount": fee_amount }), + ), + ) + .into_response(), + CalculateQuoteError::QuoteNotVerified => ( + StatusCode::BAD_REQUEST, error( "QuoteNotVerified", "No quote for this trade could be verified to be accurate. Orders for this \ trade will likely not be executed.", ), - StatusCode::BAD_REQUEST, - ), + ) + .into_response(), CalculateQuoteError::Other(err) => { tracing::error!(?err, "CalculateQuoteErrorWrapper"); crate::api::internal_error_reply() @@ -110,7 +103,6 @@ mod tests { serde_json::json, shared::order_quoting::CalculateQuoteError, std::{str::FromStr, time::Duration}, - warp::{Reply, test::request}, }; #[test] @@ -270,32 +262,6 @@ mod tests { ); } - #[tokio::test] - async fn post_quote_request_ok() { - let filter = post_quote_request(); - let request_payload = OrderQuoteRequest::default(); - let request = request() - .path("/v1/quote") - .method("POST") - .header("content-type", "application/json") - .json(&request_payload); - let result = request.filter(&filter).await.unwrap(); - assert_eq!(result, request_payload); - } - - #[tokio::test] - async fn post_quote_request_err() { - let filter = post_quote_request(); - let request_payload = OrderQuoteRequest::default(); - // Path is wrong! - let request = request() - .path("/v1/fee_quote") - .method("POST") - .header("content-type", "application/json") - .json(&request_payload); - assert!(request.filter(&filter).await.is_err()); - } - #[tokio::test] async fn post_quote_response_ok() { let quote = OrderQuote { @@ -324,10 +290,7 @@ mod tests { verified: false, protocol_fee_bps: Some("2".to_string()), }; - let response = convert_json_response::(Ok( - order_quote_response.clone(), - )) - .into_response(); + let response = (StatusCode::OK, Json(order_quote_response.clone())).into_response(); assert_eq!(response.status(), StatusCode::OK); let body = response_body(response).await; let body: serde_json::Value = serde_json::from_slice(body.as_slice()).unwrap(); @@ -337,10 +300,8 @@ mod tests { #[tokio::test] async fn post_quote_response_err() { - let response = convert_json_response::(Err( - OrderQuoteErrorWrapper(OrderQuoteError::CalculateQuote(CalculateQuoteError::Other( - anyhow!("Uh oh - error"), - ))), + let response = OrderQuoteErrorWrapper(OrderQuoteError::CalculateQuote( + CalculateQuoteError::Other(anyhow!("Uh oh - error")), )) .into_response(); assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); diff --git a/crates/orderbook/src/api/put_app_data.rs b/crates/orderbook/src/api/put_app_data.rs index 4bf9e417d2..d8f25d70b1 100644 --- a/crates/orderbook/src/api/put_app_data.rs +++ b/crates/orderbook/src/api/put_app_data.rs @@ -1,69 +1,71 @@ use { - crate::api::{IntoWarpReply, internal_error_reply}, + crate::api::{AppState, internal_error_reply}, anyhow::Result, app_data::{AppDataDocument, AppDataHash}, - reqwest::StatusCode, - std::{convert::Infallible, sync::Arc}, - warp::{Filter, Rejection, body, reply}, + axum::{ + extract::{Path, State}, + http::StatusCode, + response::{IntoResponse, Json, Response}, + }, + std::sync::Arc, }; -fn request( - max_size: usize, -) -> impl Filter, AppDataDocument), Error = Rejection> + Clone { - let opt = warp::path::param::() - .map(Some) - .or_else(|_| async { Ok::<(Option,), std::convert::Infallible>((None,)) }); - warp::path!("v1" / "app_data" / ..) - .and(opt) - .and(warp::put()) - .and(body::content_length_limit(max_size as _)) - .and(body::json()) +pub async fn put_app_data_without_hash( + State(state): State>, + Json(document): Json, +) -> Response { + let result = state + .app_data + .register(None, document.full_app_data.as_bytes()) + .await; + response(result) +} + +pub async fn put_app_data_with_hash( + State(state): State>, + Path(hash): Path, + Json(document): Json, +) -> Response { + let result = state + .app_data + .register(Some(hash), document.full_app_data.as_bytes()) + .await; + response(result) } fn response( result: Result<(crate::app_data::Registered, AppDataHash), crate::app_data::RegisterError>, -) -> super::ApiReply { +) -> Response { match result { Ok((registered, hash)) => { let status = match registered { crate::app_data::Registered::New => StatusCode::CREATED, crate::app_data::Registered::AlreadyExisted => StatusCode::OK, }; - reply::with_status(reply::json(&hash), status) + (status, Json(hash)).into_response() } - Err(err) => err.into_warp_reply(), + Err(err) => err.into_response(), } } -pub fn filter( - registry: Arc, -) -> impl Filter + Clone { - request(registry.size_limit()).and_then(move |hash, document: AppDataDocument| { - let registry = registry.clone(); - async move { - let result = registry - .register(hash, document.full_app_data.as_bytes()) - .await; - Result::<_, Infallible>::Ok(response(result)) - } - }) -} - -impl IntoWarpReply for crate::app_data::RegisterError { - fn into_warp_reply(self) -> super::ApiReply { +impl IntoResponse for crate::app_data::RegisterError { + fn into_response(self) -> Response { match self { - Self::Invalid(err) => reply::with_status( + Self::Invalid(err) => ( + StatusCode::BAD_REQUEST, super::error("AppDataInvalid", err.to_string()), + ) + .into_response(), + err @ Self::HashMismatch { .. } => ( StatusCode::BAD_REQUEST, - ), - err @ Self::HashMismatch { .. } => reply::with_status( super::error("AppDataHashMismatch", err.to_string()), + ) + .into_response(), + err @ Self::DataMismatch { .. } => ( StatusCode::BAD_REQUEST, - ), - err @ Self::DataMismatch { .. } => reply::with_status( super::error("AppDataMismatch", err.to_string()), - StatusCode::BAD_REQUEST, - ), + ) + .into_response(), Self::Other(err) => { tracing::error!(?err, "app_data::SaveError::Other"); internal_error_reply() diff --git a/crates/orderbook/src/api/version.rs b/crates/orderbook/src/api/version.rs index 04b5c698c5..e2f2e34555 100644 --- a/crates/orderbook/src/api/version.rs +++ b/crates/orderbook/src/api/version.rs @@ -1,16 +1,8 @@ -use { - reqwest::StatusCode, - std::convert::Infallible, - warp::{Filter, Rejection, Reply, reply::with_status}, +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, }; -pub fn version() -> impl Filter,), Error = Rejection> + Clone { - warp::path!("v1" / "version") - .and(warp::get()) - .and_then(|| async { - Result::<_, Infallible>::Ok(Box::new(with_status( - env!("VERGEN_GIT_DESCRIBE"), - StatusCode::OK, - )) as Box) - }) +pub async fn version_handler() -> Response { + (StatusCode::OK, env!("VERGEN_GIT_DESCRIBE")).into_response() } diff --git a/crates/orderbook/src/run.rs b/crates/orderbook/src/run.rs index 1271d956e6..90451c97c7 100644 --- a/crates/orderbook/src/run.rs +++ b/crates/orderbook/src/run.rs @@ -22,7 +22,7 @@ use { WETH9, support::Balances, }, - futures::{FutureExt, StreamExt}, + futures::StreamExt, model::{DomainSeparator, order::BUY_ETH_ADDRESS}, num::ToPrimitive, observe::metrics::{DEFAULT_METRICS_PORT, serve_metrics}, @@ -53,9 +53,8 @@ use { sources::{self, BaselineSource, uniswap_v2::UniV2BaselineSourceParameters}, token_info::{CachedTokenInfoFetcher, TokenInfoFetcher}, }, - std::{convert::Infallible, future::Future, net::SocketAddr, sync::Arc, time::Duration}, + std::{future::Future, net::SocketAddr, sync::Arc, time::Duration}, tokio::task::{self, JoinHandle}, - warp::Filter, }; pub async fn start(args: impl Iterator) { @@ -323,6 +322,7 @@ pub async fn run(args: Arguments) { ) .await .unwrap(); + // NOTE for reviewers: this could this be postgres_read (?) let prices = postgres_write.fetch_latest_prices().await.unwrap(); native_price_estimator.initialize_cache(prices); @@ -541,7 +541,7 @@ fn serve_api( native_price_estimator: Arc, quote_timeout: Duration, ) -> JoinHandle<()> { - let filter = api::handle_all_routes( + let app = api::handle_all_routes( database, database_replica, orderbook, @@ -549,19 +549,18 @@ fn serve_api( app_data, native_price_estimator, quote_timeout, - ) - .boxed(); + ); tracing::info!(%address, "serving order book"); - let warp_svc = warp::service(filter); - let make_svc = hyper::service::make_service_fn(move |_| { - let svc = warp_svc.clone(); - async move { Ok::<_, Infallible>(svc) } - }); - let server = hyper::Server::bind(&address) - .serve(make_svc) - .with_graceful_shutdown(shutdown_receiver) - .map(|_| ()); - task::spawn(server) + + let server = axum::Server::bind(&address) + .serve(app.into_make_service()) + .with_graceful_shutdown(shutdown_receiver); + + task::spawn(async move { + if let Err(err) = server.await { + tracing::error!(?err, "server error"); + } + }) } /// Check that important constants such as the EIP 712 Domain Separator and diff --git a/crates/solvers/src/api/routes/healthz.rs b/crates/solvers/src/api/routes/healthz.rs index 7e39e585dc..fc34fe28c8 100644 --- a/crates/solvers/src/api/routes/healthz.rs +++ b/crates/solvers/src/api/routes/healthz.rs @@ -1,5 +1,8 @@ -use axum::{http::StatusCode, response::IntoResponse}; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; -pub async fn healthz() -> impl IntoResponse { - StatusCode::OK +pub async fn healthz() -> Response { + StatusCode::OK.into_response() } diff --git a/playground/docker-compose.non-interactive.yml b/playground/docker-compose.non-interactive.yml index c9d5fca62f..356fc8652b 100644 --- a/playground/docker-compose.non-interactive.yml +++ b/playground/docker-compose.non-interactive.yml @@ -115,7 +115,7 @@ services: - NODE_URL=http://chain:8545 - SIMULATION_NODE_URL=http://chain:8545 - SETTLE_INTERVAL=15s - - GAS_ESTIMATORS=Web3 + - GAS_ESTIMATORS=Web3,Alloy - PRICE_ESTIMATORS=None - NATIVE_PRICE_ESTIMATORS=baseline - BLOCK_STREAM_POLL_INTERVAL=1s