1use std::time::Duration;
2
3use crate::ws::client::{PreparedClientMessage, WsTransportConfig};
4use crate::ws::error::WsTransportConfigError;
5use crate::ws::types::{ClientMessage, FEATURE_CANCEL_ON_DISCONNECT, HelloData, SubscribeData};
6
7use super::endpoint::MakerWsEndpoint;
8use super::signer::ChallengeSigning;
9use crate::orders::SignerLike;
10use std::sync::Arc;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18#[non_exhaustive]
19pub enum GapPolicy {
20 #[default]
22 EndpointDefault,
23 Surface,
24 Reconnect,
25}
26
27const MAX_INBOUND_RING_SLOTS: usize = 65_536;
28const MAX_OUTBOUND_QUEUE_WIRE_BYTES: usize = 256 * 1024 * 1024;
29const SERVER_MAX_BATCH_QUOTES: usize = 50;
30const SERVER_MAX_CLIENT_MESSAGE_BYTES: usize = 32 * 1024;
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub(crate) enum AuthFlow {
35 Maker,
37 #[cfg_attr(not(feature = "vault"), allow(dead_code))]
40 Vault { vault_pda: String },
41}
42
43#[derive(Clone)]
44pub struct ManagedWsConfig {
45 pub url: String,
46 pub endpoint: MakerWsEndpoint,
47 pub(super) auth_pubkey: String,
48 pub(super) challenge_signing: ChallengeSigning,
49 pub(crate) auth_flow: AuthFlow,
50 pub(super) initial_subscribe: Option<SubscribeData>,
51 pub reconnect_delay: Duration,
52 pub max_reconnect_delay: Duration,
53 pub reconnect_jitter_ratio: f64,
55 pub max_reconnect_attempts: u64,
57 pub connect_timeout: Duration,
59 pub ping_interval: Duration,
60 pub auth_timeout: Duration,
61 pub readiness_timeout: Duration,
64 pub write_timeout: Duration,
65 pub command_buffer: usize,
66 pub broadcast_buffer: usize,
67 pub max_batch_quotes: usize,
69 pub max_outbound_message_size: usize,
71 pub ws_read_timeout: Option<Duration>,
72 pub pong_timeout: Duration,
73 pub transport: WsTransportConfig,
74 pub max_pending_awaits: usize,
75 pub cancel_on_disconnect: bool,
79 pub gap_policy: GapPolicy,
80 pub(super) hello: HelloData,
81}
82
83impl ManagedWsConfig {
84 #[must_use]
87 pub fn new(
88 url: impl Into<String>,
89 hello: HelloData,
90 signer: Arc<dyn SignerLike + Send + Sync>,
91 auth_pubkey: impl Into<String>,
92 ) -> Self {
93 Self::new_with_signing(
94 url,
95 hello,
96 auth_pubkey.into(),
97 ChallengeSigning::Local(signer),
98 )
99 }
100
101 pub(crate) fn new_with_signing(
102 url: impl Into<String>,
103 hello: HelloData,
104 auth_pubkey: String,
105 challenge_signing: ChallengeSigning,
106 ) -> Self {
107 Self {
108 url: url.into(),
109 endpoint: MakerWsEndpoint::Quote,
110 auth_pubkey,
111 challenge_signing,
112 auth_flow: AuthFlow::Maker,
113 initial_subscribe: None,
114 reconnect_delay: Duration::from_millis(250),
115 max_reconnect_delay: Duration::from_secs(5),
116 reconnect_jitter_ratio: 0.2,
117 max_reconnect_attempts: 0,
118 connect_timeout: Duration::from_secs(10),
119 ping_interval: Duration::from_secs(30),
120 auth_timeout: Duration::from_secs(15),
121 readiness_timeout: Duration::from_secs(15),
122 write_timeout: Duration::from_secs(5),
123 command_buffer: 256,
124 broadcast_buffer: 4096,
125 max_batch_quotes: SERVER_MAX_BATCH_QUOTES,
126 max_outbound_message_size: SERVER_MAX_CLIENT_MESSAGE_BYTES,
127 ws_read_timeout: None,
128 pong_timeout: Duration::from_secs(10),
129 transport: WsTransportConfig::default(),
130 max_pending_awaits: 1024,
131 cancel_on_disconnect: true,
132 gap_policy: GapPolicy::EndpointDefault,
133 hello,
134 }
135 }
136
137 #[must_use]
139 pub const fn low_latency(mut self) -> Self {
140 self.ping_interval = Duration::from_secs(2);
141 self.pong_timeout = Duration::from_secs(2);
142 self.write_timeout = Duration::from_secs(1);
143 self.command_buffer = 1024;
144 self.gap_policy = GapPolicy::Reconnect;
145 self
146 }
147
148 #[must_use]
149 pub const fn with_gap_policy(mut self, policy: GapPolicy) -> Self {
150 self.gap_policy = policy;
151 self
152 }
153
154 #[must_use]
155 pub const fn with_cancel_on_disconnect(mut self, enabled: bool) -> Self {
156 self.cancel_on_disconnect = enabled;
157 self
158 }
159
160 pub(super) fn reconnect_on_gap(&self) -> bool {
161 match self.gap_policy {
162 GapPolicy::EndpointDefault => self.endpoint == MakerWsEndpoint::Quote,
163 GapPolicy::Surface => false,
164 GapPolicy::Reconnect => true,
165 }
166 }
167
168 pub(super) fn hello(&self) -> HelloData {
170 let mut hello = self.hello.clone();
171 if self.endpoint == MakerWsEndpoint::Quote
172 && self.cancel_on_disconnect
173 && !hello
174 .features
175 .iter()
176 .any(|f| f == FEATURE_CANCEL_ON_DISCONNECT)
177 {
178 hello.features.push(FEATURE_CANCEL_ON_DISCONNECT.to_owned());
179 }
180 hello
181 }
182
183 pub(super) fn missing_required_feature(&self, enabled: &[String]) -> Option<&'static str> {
185 (self.endpoint == MakerWsEndpoint::Quote
186 && self.cancel_on_disconnect
187 && !enabled.iter().any(|f| f == FEATURE_CANCEL_ON_DISCONNECT))
188 .then_some(FEATURE_CANCEL_ON_DISCONNECT)
189 }
190
191 #[must_use]
192 pub const fn with_endpoint(mut self, endpoint: MakerWsEndpoint) -> Self {
193 self.endpoint = endpoint;
194 self
195 }
196
197 #[cfg(feature = "vault")]
199 pub(crate) fn with_vault_auth(mut self, vault_pda: String) -> Self {
200 self.auth_flow = AuthFlow::Vault { vault_pda };
201 self.initial_subscribe = Some(SubscribeData {
202 request_id: uuid::Uuid::new_v4(),
203 channels: vec![crate::ws::types::WsChannel::Vaults],
204 underlying_mints: None,
205 quote_mints: None,
206 });
207 self.with_endpoint(MakerWsEndpoint::Vault)
208 }
209
210 pub(crate) fn vault_pda(&self) -> Option<&str> {
211 match &self.auth_flow {
212 AuthFlow::Vault { vault_pda } => Some(vault_pda),
213 AuthFlow::Maker => None,
214 }
215 }
216
217 #[must_use]
218 pub fn with_initial_subscribe(mut self, subscribe: SubscribeData) -> Self {
219 self.initial_subscribe = Some(subscribe);
220 self
221 }
222
223 pub fn validate(&self) -> Result<(), ManagedWsConfigError> {
225 if self.url.trim().is_empty() {
226 return Err(ManagedWsConfigError::EmptyUrl);
227 }
228 for (field, capacity) in [
229 ("command_buffer", self.command_buffer),
230 ("broadcast_buffer", self.broadcast_buffer),
231 ("max_pending_awaits", self.max_pending_awaits),
232 ("max_batch_quotes", self.max_batch_quotes),
233 ("max_outbound_message_size", self.max_outbound_message_size),
234 ] {
235 if capacity == 0 {
236 return Err(ManagedWsConfigError::ZeroCapacity { field });
237 }
238 }
239 for (field, duration) in [
240 ("reconnect_delay", self.reconnect_delay),
241 ("max_reconnect_delay", self.max_reconnect_delay),
242 ("connect_timeout", self.connect_timeout),
243 ("ping_interval", self.ping_interval),
244 ("auth_timeout", self.auth_timeout),
245 ("readiness_timeout", self.readiness_timeout),
246 ("write_timeout", self.write_timeout),
247 ("pong_timeout", self.pong_timeout),
248 ] {
249 if duration.is_zero() {
250 return Err(ManagedWsConfigError::ZeroDuration { field });
251 }
252 }
253 if self
254 .ws_read_timeout
255 .is_some_and(|duration| duration.is_zero())
256 {
257 return Err(ManagedWsConfigError::ZeroDuration {
258 field: "ws_read_timeout",
259 });
260 }
261 if self.reconnect_delay > self.max_reconnect_delay {
262 return Err(ManagedWsConfigError::InvalidReconnectRange);
263 }
264 if !(0.0..=1.0).contains(&self.reconnect_jitter_ratio) {
265 return Err(ManagedWsConfigError::InvalidReconnectJitter);
266 }
267 validate_server_limit(
268 "max_batch_quotes",
269 self.max_batch_quotes,
270 SERVER_MAX_BATCH_QUOTES,
271 )?;
272 validate_server_limit(
273 "max_outbound_message_size",
274 self.max_outbound_message_size,
275 SERVER_MAX_CLIENT_MESSAGE_BYTES,
276 )?;
277 if self.endpoint == MakerWsEndpoint::Data && self.initial_subscribe.is_some() {
278 return Err(ManagedWsConfigError::InitialSubscribeOnDataEndpoint);
279 }
280 if (self.endpoint == MakerWsEndpoint::Vault) != self.vault_pda().is_some() {
281 return Err(ManagedWsConfigError::VaultEndpointAuthMismatch);
282 }
283 if let Some(subscribe) = &self.initial_subscribe {
284 let message = ClientMessage::Subscribe(subscribe.clone());
285 let prepared = PreparedClientMessage::new(&message)
286 .map_err(|_| ManagedWsConfigError::InitialSubscribeNotSerializable)?;
287 if prepared.len() > self.max_outbound_message_size {
288 return Err(ManagedWsConfigError::InitialSubscribeTooLarge {
289 actual: prepared.len(),
290 limit: self.max_outbound_message_size,
291 });
292 }
293 }
294 self.effective_transport().validate()?;
295 validate_slot_cap(
296 "broadcast_buffer",
297 self.broadcast_buffer,
298 MAX_INBOUND_RING_SLOTS,
299 )?;
300 validate_memory_envelope(
301 "outbound command queue",
302 self.command_buffer,
303 self.max_outbound_message_size,
304 MAX_OUTBOUND_QUEUE_WIRE_BYTES,
305 )
306 }
307
308 pub(super) const fn effective_transport(&self) -> WsTransportConfig {
309 let mut transport = self.transport;
310 transport.connect_timeout = self.connect_timeout;
311 transport
312 }
313}
314
315fn validate_server_limit(
316 field: &'static str,
317 configured: usize,
318 limit: usize,
319) -> Result<(), ManagedWsConfigError> {
320 if configured > limit {
321 return Err(ManagedWsConfigError::ProtocolLimitExceeded {
322 field,
323 configured,
324 limit,
325 });
326 }
327 Ok(())
328}
329
330fn validate_slot_cap(
332 field: &'static str,
333 capacity: usize,
334 limit: usize,
335) -> Result<(), ManagedWsConfigError> {
336 if capacity > limit {
337 return Err(ManagedWsConfigError::CapacityTooLarge {
338 field,
339 configured: capacity,
340 limit,
341 });
342 }
343 Ok(())
344}
345
346fn validate_memory_envelope(
350 queue: &'static str,
351 capacity: usize,
352 max_message_size: usize,
353 limit: usize,
354) -> Result<(), ManagedWsConfigError> {
355 let configured = capacity.saturating_mul(max_message_size);
356 if configured > limit {
357 return Err(ManagedWsConfigError::MemoryEnvelopeTooLarge {
358 queue,
359 configured,
360 limit,
361 });
362 }
363 Ok(())
364}
365
366#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
367#[non_exhaustive]
368pub enum ManagedWsConfigError {
369 #[error("managed websocket URL must not be empty")]
370 EmptyUrl,
371 #[error("managed websocket capacity `{field}` must be non-zero")]
372 ZeroCapacity { field: &'static str },
373 #[error("managed websocket duration `{field}` must be non-zero")]
374 ZeroDuration { field: &'static str },
375 #[error("maximum reconnect delay must not be shorter than the initial reconnect delay")]
376 InvalidReconnectRange,
377 #[error("reconnect jitter ratio must be in the inclusive range 0.0..=1.0")]
378 InvalidReconnectJitter,
379 #[error("initial subscriptions are not supported by the maker data endpoint")]
380 InitialSubscribeOnDataEndpoint,
381 #[error("the vault endpoint requires the vault auth flow, and vice versa")]
382 VaultEndpointAuthMismatch,
383 #[error("initial subscription is {actual} bytes, maximum is {limit}")]
384 InitialSubscribeTooLarge { actual: usize, limit: usize },
385 #[error("{field} is {configured}, server maximum is {limit}")]
386 ProtocolLimitExceeded {
387 field: &'static str,
388 configured: usize,
389 limit: usize,
390 },
391 #[error("initial subscribe cannot be serialized")]
392 InitialSubscribeNotSerializable,
393 #[error("{field} is {configured}, maximum is {limit}")]
394 CapacityTooLarge {
395 field: &'static str,
396 configured: usize,
397 limit: usize,
398 },
399 #[error("{queue} wire envelope is {configured} bytes, maximum is {limit}")]
400 MemoryEnvelopeTooLarge {
401 queue: &'static str,
402 configured: usize,
403 limit: usize,
404 },
405 #[error(transparent)]
406 Transport(#[from] WsTransportConfigError),
407}