From 854e4800d16d9b86b48c4cac06aa99451c3f14b0 Mon Sep 17 00:00:00 2001 From: ioanSL Date: Thu, 10 Sep 2026 13:06:48 +0300 Subject: [PATCH] feat(webhook-api): add optional source to quote/swap requests and responses Co-Authored-By: Claude Fable 5.1 --- server-example/src/server.rs | 6 ++++++ webhook-api/src/requests.rs | 27 +++++++++++++++++++++++++++ webhook-api/src/responses.rs | 8 ++++++++ 3 files changed, 41 insertions(+) diff --git a/server-example/src/server.rs b/server-example/src/server.rs index cb93af0..bb1c527 100644 --- a/server-example/src/server.rs +++ b/server-example/src/server.rs @@ -172,6 +172,7 @@ async fn example_quote( maker: maker_pubkey, prioritization_fee_to_use: quote_request.suggested_prioritization_fees, receiver: quote_request.receiver.clone(), + source: quote_request.source.clone(), } } QuoteType::ExactOut => { @@ -189,6 +190,7 @@ async fn example_quote( maker: maker_pubkey, prioritization_fee_to_use: quote_request.suggested_prioritization_fees, receiver: quote_request.receiver.clone(), + source: quote_request.source.clone(), } } }; @@ -234,18 +236,21 @@ async fn example_swap( quote_id: quote_request.quote_id.clone(), state: SwapState::Rejected, rejection_reason: Some("".to_string()), + source: quote_request.source.clone(), })), SIMULATE_INSUFFICIENT_BALANCE => Ok(Json(SwapResponse { tx_signature: None, quote_id: quote_request.quote_id.clone(), state: SwapState::RejectedWithReason(RejectionReason::InsufficientBalance), rejection_reason: None, + source: quote_request.source.clone(), })), SIMULATE_SIGNATURE_VERIFICATION_FAILED => Ok(Json(SwapResponse { tx_signature: None, quote_id: quote_request.quote_id.clone(), state: SwapState::RejectedWithReason(RejectionReason::SignatureVerificationFailed), rejection_reason: None, + source: quote_request.source.clone(), })), SIMULATE_MALFORMED => Err(ApiError::BadRequest("Malformed request".to_string())), _ => { @@ -306,6 +311,7 @@ async fn example_swap( quote_id: quote_request.quote_id.clone(), state: SwapState::Accepted, rejection_reason: None, + source: quote_request.source.clone(), })) } } diff --git a/webhook-api/src/requests.rs b/webhook-api/src/requests.rs index 83de1e9..3fe450c 100644 --- a/webhook-api/src/requests.rs +++ b/webhook-api/src/requests.rs @@ -43,6 +43,11 @@ pub struct QuoteRequest { #[schema(examples("5v2Vd71VoJ1wZhz1PkhTY48mrJwS6wF4LfvDbYPnJ3bc"))] #[serde(skip_serializing_if = "Option::is_none")] pub receiver: Option, + /// Where the request originated: `internal` (a Jupiter product) or `integrator` (a third-party + /// integrator). Set by the RFQ API; any value supplied by a taker is ignored. + #[schema(examples("internal", "integrator"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, } /// Order to be fulfilled by the Market Maker @@ -56,4 +61,26 @@ pub struct SwapRequest { /// Base64 encoded versioned transaction #[schema(examples("AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAz9drCWYYQ68kASuBn9OHQMhApFELvj44L1s76RFusqcFQ3u65aig44TQ3Fmb9CadUg6y5zJuBNnD1IxqvKXIPmA4AmO5Dcos4MycwafOIB13mDRFQ1GIRqKG3olkhi48jyGiqvTscHPp0TmqflJdR4gzVibQqwIj1iO1jXHw5Mt99q5m2Edp3glkLYOc/yT1HqD+ndBXyPYu16F84mC8rspYEafRZphIlog6Q2qO4TFgN8ICPW2yl1kkJ2UutYEAxh1w4ztXWtKZr0O736NcYMPOKkRjP8CiDXheWMdaprkzkaA5jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAvEC3LPJppXp/7XKg0lfT6E87hQi8th5zPmi1Q6AAAAAiDuU6+gSRr+Hx/3xlmJE16rC5kSGnxcWkohAAAAAAAg5izz2u1OeCnop+hOiUf4tBZ6V2mJYyUZ0OXgw2U/A4C33oSysI64Na/dJmwBs/WKYt6Nnkl1JWNR65pjlN4nAKWsCeLSm9f59f75OR3BSLGqzUjgzq3orAORpoFbS1sy0skiTfwZdbBKqGpHSo0ZZfwJkHDO1fB4frglAsUPj0YoKeHP5JwEP4awSwmz4vanicFfQDIeA6ZM8UhZEZe0FLBZAAyNq5O0AAAAAAA2AAagQ9iAAAlYAMq28zUDAlb3EiKAFj7JAPtooroIJbdZdUpVUmdUfgBMAAAAAAH4YYwAAAAAAC9MP4tAAAAAAA=="))] pub transaction: String, + /// Where the request originated: `internal` (a Jupiter product) or `integrator` (a third-party + /// integrator). Set by the RFQ API; any value supplied by a taker is ignored. + #[schema(examples("internal", "integrator"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn source_is_optional_on_the_wire() { + let json = r#"{"requestId":"r","quoteId":"q","tokenIn":"a","amount":"1","tokenOut":"b","quoteType":"exactIn","protocol":"v1","feeBps":0}"#; + let req: QuoteRequest = serde_json::from_str(json).unwrap(); + assert_eq!(req.source, None); + assert!(!serde_json::to_string(&req).unwrap().contains("source")); + + let with_source = format!(r#"{{"source":"integrator",{}"#, &json[1..]); + let req: QuoteRequest = serde_json::from_str(&with_source).unwrap(); + assert_eq!(req.source.as_deref(), Some("integrator")); + } } diff --git a/webhook-api/src/responses.rs b/webhook-api/src/responses.rs index cd4267e..ca38183 100644 --- a/webhook-api/src/responses.rs +++ b/webhook-api/src/responses.rs @@ -39,6 +39,10 @@ pub struct QuoteResponse { #[schema(examples("5v2Vd71VoJ1wZhz1PkhTY48mrJwS6wF4LfvDbYPnJ3bc"))] #[serde(skip_serializing_if = "Option::is_none")] pub receiver: Option, + /// Echo of the request's `source`, when provided. + #[schema(examples("internal", "integrator"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, } /// Response to a swap request to the Market Maker @@ -55,6 +59,10 @@ pub struct SwapResponse { /// Optional message to provide more context when the swap is rejected #[serde(skip_serializing_if = "Option::is_none")] pub rejection_reason: Option, + /// Echo of the request's `source`, when provided. + #[schema(examples("internal", "integrator"))] + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, } #[derive(Clone, Debug, Deserialize, Serialize, ToSchema)]