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