Skip to content

[streaming-quote-api] SSE endpoint (3/3)#4469

Open
squadgazzz wants to merge 23 commits into
feat/streaming-quote-assemblyfrom
feat/streaming-quote-endpoint
Open

[streaming-quote-api] SSE endpoint (3/3)#4469
squadgazzz wants to merge 23 commits into
feat/streaming-quote-assemblyfrom
feat/streaming-quote-endpoint

Conversation

@squadgazzz
Copy link
Copy Markdown
Contributor

@squadgazzz squadgazzz commented Jun 3, 2026

Description

Third and final PR of the streaming quote API stack (#4456). Adds the SSE endpoint POST /api/v1/quote/stream that 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 /order is unchanged.

The priceQuality field of OrderQuoteRequest is ignored on this endpoint: streaming always queries all solvers and attempts verification, emitting each result with its own verified flag. The field stays in the body only because the endpoint reuses OrderQuoteRequest. This is documented in the OpenAPI description.

Changes

  • Forward streaming through SanitizedPriceEstimator so token sanitization applies per result.
  • Add a streaming_price_estimator factory 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/stream SSE 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 terminal error event is sent (a NoLiquidity body routed through the same mapping the regular endpoint uses), otherwise the stream closes cleanly.
  • Wire the streaming estimator into the optimal quoter and the quote handler.
  • OpenAPI docs for the new endpoint, including that priceQuality is 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 e2e is currently broken on main (pre-existing, from the SameTokensPolicy change in #4463), so the new e2e test could not be compile-verified on this branch. The unit tests and cargo build -p orderbook pass.

Related issues

Closes #4456

squadgazzz added 11 commits June 3, 2026 20:37
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.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

Reminder: Please consider backward compatibility when modifying the API specification.
If breaking changes are unavoidable, ensure:

  • You explicitly pointed out breaking changes.
  • You communicate the changes to affected teams (at least Frontend team and SAFE team).
  • You provide proper versioning and migration mechanisms.

Caused by:

@squadgazzz squadgazzz changed the title Streaming quote API: SSE endpoint (3/3) [streaming-quote-api] SSE endpoint (3/3) Jun 4, 2026
@squadgazzz squadgazzz marked this pull request as ready for review June 5, 2026 10:54
@squadgazzz squadgazzz requested a review from a team as a code owner June 5, 2026 10:54
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +46 to +57
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"),
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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"),
            }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant