[streaming-quote-api] SSE endpoint (3/3)#4469
Conversation
Adds StreamingPriceEstimating support to OrderQuoter: a new optional streaming_price_estimator field (set via with_streaming_estimator builder), a StreamingQuoting trait, and its impl that fetches gas + native prices once then yields one Quote per solver result using async-stream.
Extracts the shared validation preamble into `build_quote_params` and the response construction into `build_order_quote_response`, then adds `calculate_quote_stream` which runs the same preamble, delegates to a `StreamingQuoting` impl, and maps each yielded `Quote` to an `OrderQuoteResponse` (id: None, volume fee applied per item). Adds `async-stream` to orderbook Cargo.toml (workspace dep).
Handler runs the same validation as POST /api/v1/quote (prelude errors return identical HTTP 4xx). Successful quotes stream as SSE events; per-item errors are dropped with a debug log. If no quote succeeds, emits a final SSE error event with NoLiquidity.
|
Reminder: Please consider backward compatibility when modifying the API specification.
Caused by: |
…ent streaming 404/422
… log and comments
There was a problem hiding this comment.
Code Review
This pull request introduces a Server-Sent Events (SSE) streaming quote endpoint (/api/v1/quote/stream) to stream quotes from individual solvers in real time. The changes include adding the async-stream dependency, implementing streaming traits and handlers across the orderbook and price-estimation crates, updating the OpenAPI specification, and adding integration tests. Feedback highlights a high-severity issue in post_quote_stream_handler where converting the NoLiquidity error into an axum::Response and reading its body asynchronously introduces unnecessary complexity and overhead; serializing the error payload directly to a JSON string is recommended instead.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| let response = | ||
| super::PriceEstimationErrorWrapper(PriceEstimationError::NoLiquidity).into_response(); | ||
| match axum::body::to_bytes(response.into_body(), usize::MAX).await { | ||
| Ok(bytes) => { | ||
| yield Ok::<_, Infallible>( | ||
| Event::default() | ||
| .event("error") | ||
| .data(String::from_utf8_lossy(&bytes)), | ||
| ) | ||
| } | ||
| Err(err) => tracing::error!(?err, "failed to read no-quote error event body"), | ||
| } |
There was a problem hiding this comment.
The current implementation converts the NoLiquidity error into a full axum::Response, reads its body asynchronously using axum::body::to_bytes, and then converts it back to a string. This introduces unnecessary complexity, async overhead, and heap allocations.
Instead, you can serialize the error payload directly to a JSON string using serde_json.
let error_payload = serde_json::json!({
"errorType": "NoLiquidity",
"description": "no route found"
});
match serde_json::to_string(&error_payload) {
Ok(data) => {
yield Ok::<_, Infallible>(
Event::default()
.event("error")
.data(data),
)
}
Err(err) => tracing::error!(?err, "failed to serialize no-quote error event"),
}There was a problem hiding this comment.
I didn't want to do that manually. In case the NoLiquidity changes in some way, we would need to update the manual implementation also.
Description
Third and final PR of the streaming quote API stack (#4456). Adds the SSE endpoint
POST /api/v1/quote/streamthat emits one quote per solver as it arrives, wiring together the competition streaming primitive (#4467) and the reusable quote assembly (#4468). Stacked on #4468.Compute is identical to the optimal quoter (all solvers plus verification). We change only when results are delivered, not their quality. Nothing is persisted, every event carries
id: null, and order posting re-quotes the same way it already does for fast quotes, so/orderis unchanged.The
priceQualityfield ofOrderQuoteRequestis ignored on this endpoint: streaming always queries all solvers and attempts verification, emitting each result with its ownverifiedflag. The field stays in the body only because the endpoint reusesOrderQuoteRequest. This is documented in the OpenAPI description.Changes
SanitizedPriceEstimatorso token sanitization applies per result.streaming_price_estimatorfactory builder.OrderQuoter::calculate_quote_stream: fetch gas and native prices once, then stream one quote per solver result, dropping unreasonable (zero-gas or zero-amount) estimates.QuoteHandler::calculate_quote_stream: shared validation preamble, volume fee applied per event.POST /api/v1/quote/streamSSE handler: validation failures return HTTP 4xx before the stream opens, per-solver errors are logged and dropped, and if no solver returns a usable quote a single terminalerrorevent is sent (aNoLiquiditybody routed through the same mapping the regular endpoint uses), otherwise the stream closes cleanly.priceQualityis ignored.How to test
New unit tests across price-estimation, shared, and orderbook. A new ignored e2e smoke test (
quote_stream_smoke).Note:
cargo check --tests -p e2eis currently broken onmain(pre-existing, from theSameTokensPolicychange in #4463), so the new e2e test could not be compile-verified on this branch. The unit tests andcargo build -p orderbookpass.Related issues
Closes #4456