1use std::sync::Arc;
2use std::time::Duration;
3
4use tokio::sync::{broadcast, watch};
5use uuid::Uuid;
6
7use crate::orders::SignerLike;
8use crate::quote::{QuoteBuildError, QuoteBuilder};
9use crate::types::VaultDelegatePermissions;
10use crate::types::ids::OrderId;
11use crate::wire::{WireError, decode_base58_32};
12use crate::ws::managed::{
13 ManagedMessageReceiver, ManagedWsConfigError, ManagedWsError, ManagedWsEvent, ManagedWsHandle,
14 ManagedWsState, ReferenceChangeReceiver, SendAwaitError, SendTicket, WaitUntilReadyError,
15 spawn_managed_ws,
16};
17use crate::ws::types::{
18 AcceptQuoteMessage, BatchQuotesMessage, CancelAllQuotesMessage, CancelQuoteData, CancelRfqData,
19 ClientMessage, Delivery, GetSwapStatusMessage, QuoteMessage, ReplaceQuoteMessage,
20 RfqRequestMessage, ServerError, ServerMessage, SponsoredTxToSignData,
21 SubmitSignedSponsoredTxData, SubscribeData, SwapQuoteData, SwapQuoteResultData,
22 SwapStatusMessage, SwapSubmitData, SwapSubmitResultData, UnsubscribeData, VaultAuthSuccessData,
23};
24
25use solana_sdk::message::AddressLookupTableAccount;
26
27use crate::chain::vault::{
28 ComputeBudgetBounds, VaultOpenIntent, VaultSwapIntent, verify_vault_open, verify_vault_swap,
29};
30
31use super::config::VaultOperatorConfig;
32use super::gate::SendGate;
33use super::projection::VaultProjectionReceiver;
34use super::tx::{VaultTxError, ensure_deadline_ahead, ensure_system_time_ahead};
35
36#[derive(Debug, thiserror::Error)]
37#[non_exhaustive]
38pub enum VaultClientError {
39 #[error("vault_pda is not a base58 32-byte address")]
40 InvalidVaultPda(#[source] WireError),
41 #[error(transparent)]
42 InvalidConfig(#[from] ManagedWsConfigError),
43}
44
45#[derive(Debug, thiserror::Error)]
46#[non_exhaustive]
47pub enum VaultRequestError {
48 #[error(transparent)]
49 Session(#[from] ManagedWsError),
50 #[error(transparent)]
51 Transport(#[from] SendAwaitError),
52 #[error("server rejected request {request_id}")]
53 Rejected {
54 request_id: Uuid,
55 response: Arc<ServerMessage>,
56 },
57 #[error("expected {expected}, received a different correlated response")]
58 UnexpectedResponse {
59 expected: &'static str,
60 response: Arc<ServerMessage>,
61 },
62 #[error("the send gate refused the command")]
63 Gated,
64}
65
66impl VaultRequestError {
67 #[must_use]
68 pub fn server_error(&self) -> Option<&ServerError> {
69 match self {
70 Self::Rejected { response, .. } => match &**response {
71 ServerMessage::RequestError(error) => Some(&error.error),
72 _ => None,
73 },
74 _ => None,
75 }
76 }
77
78 #[must_use]
81 pub fn delivery(&self) -> Option<Delivery> {
82 match self {
83 Self::Rejected { response, .. } => match &**response {
84 ServerMessage::RequestError(error) => error.delivery,
85 _ => Some(Delivery::Unknown),
86 },
87 Self::Session(ManagedWsError::NotReady | ManagedWsError::QueueFull)
88 | Self::Gated
89 | Self::Transport(
90 SendAwaitError::SubscriptionPending
91 | SendAwaitError::NotReady
92 | SendAwaitError::NoCorrelationKey
93 | SendAwaitError::DuplicateInFlight
94 | SendAwaitError::TooManyPending { .. }
95 | SendAwaitError::QueueFull
96 | SendAwaitError::InvalidMessage(_),
97 ) => Some(Delivery::NotForwarded),
98 Self::Session(_) | Self::Transport(_) | Self::UnexpectedResponse { .. } => {
99 Some(Delivery::Unknown)
100 }
101 }
102 }
103}
104
105#[derive(Clone)]
107pub struct VaultOperatorClient {
108 inner: ManagedWsHandle,
109 vault_pda: String,
110 vault_owner: [u8; 32],
111 operator: Arc<dyn SignerLike + Send + Sync>,
112 request_timeout: Duration,
113}
114
115impl VaultOperatorClient {
116 pub fn spawn(config: VaultOperatorConfig) -> Result<Self, VaultClientError> {
117 let vault_owner =
118 decode_base58_32(&config.vault_pda).map_err(VaultClientError::InvalidVaultPda)?;
119 Ok(Self {
120 inner: spawn_managed_ws(config.managed)?,
121 vault_pda: config.vault_pda,
122 vault_owner,
123 operator: config.operator,
124 request_timeout: config.request_timeout,
125 })
126 }
127
128 #[must_use]
129 pub fn vault_pda(&self) -> &str {
130 &self.vault_pda
131 }
132
133 #[must_use]
135 pub const fn vault_owner(&self) -> [u8; 32] {
136 self.vault_owner
137 }
138
139 #[must_use]
140 pub fn operator_pubkey(&self) -> String {
141 self.operator.pubkey_base58()
142 }
143
144 #[must_use]
146 pub fn auth(&self) -> Option<VaultAuthSuccessData> {
147 self.inner.vault_auth()
148 }
149
150 #[must_use]
151 pub fn permissions(&self) -> Option<VaultDelegatePermissions> {
152 self.auth().map(|auth| auth.permissions)
153 }
154
155 #[must_use]
156 pub fn can_maker(&self) -> bool {
157 self.inner.is_ready()
158 && self.auth().is_some_and(|auth| {
159 matches!(
160 auth.operator_kind,
161 crate::types::VaultOperatorKind::Delegate
162 | crate::types::VaultOperatorKind::Governance
163 ) && auth.permissions.can_maker()
164 })
165 }
166
167 #[must_use]
168 pub fn can_taker(&self) -> bool {
169 self.inner.is_ready()
170 && self
171 .permissions()
172 .is_some_and(VaultDelegatePermissions::can_taker)
173 }
174
175 #[must_use]
176 pub fn subscribe_vault_auth(&self) -> watch::Receiver<Option<VaultAuthSuccessData>> {
177 self.inner.subscribe_vault_auth()
178 }
179
180 pub fn sign_quote(&self, quote: &QuoteBuilder<'_>) -> Result<QuoteMessage, QuoteBuildError> {
181 quote
182 .clone()
183 .maker_owner(self.vault_owner)
184 .sign(self.operator.as_ref())
185 }
186
187 pub async fn quote_in_epoch(
190 &self,
191 payload: QuoteMessage,
192 connection_epoch: u64,
193 ) -> Result<(), ManagedWsError> {
194 self.inner
195 .send_in_epoch(ClientMessage::Quote(payload), connection_epoch)
196 .await
197 }
198
199 pub fn try_quote_in_epoch(
200 &self,
201 payload: QuoteMessage,
202 connection_epoch: u64,
203 ) -> Result<SendTicket, ManagedWsError> {
204 self.inner
205 .try_send_in_epoch(ClientMessage::Quote(payload), connection_epoch)
206 }
207
208 pub async fn replace_in_epoch(
209 &self,
210 payload: ReplaceQuoteMessage,
211 connection_epoch: u64,
212 ) -> Result<(), ManagedWsError> {
213 self.inner
214 .send_in_epoch(ClientMessage::ReplaceQuote(payload), connection_epoch)
215 .await
216 }
217
218 pub async fn batch_quotes_in_epoch(
219 &self,
220 payload: BatchQuotesMessage,
221 connection_epoch: u64,
222 ) -> Result<(), ManagedWsError> {
223 self.inner
224 .send_in_epoch(ClientMessage::BatchQuotes(payload), connection_epoch)
225 .await
226 }
227
228 pub async fn cancel_quote(&self, payload: CancelQuoteData) -> Result<(), ManagedWsError> {
229 self.send(ClientMessage::CancelQuote(payload)).await
230 }
231
232 pub async fn cancel_all_quotes(
233 &self,
234 payload: CancelAllQuotesMessage,
235 ) -> Result<(), ManagedWsError> {
236 self.send(ClientMessage::CancelAllQuotes(payload)).await
237 }
238
239 pub async fn rfq_request(&self, payload: RfqRequestMessage) -> Result<(), VaultRequestError> {
241 self.command(ClientMessage::RfqRequest(payload)).await
242 }
243
244 pub async fn cancel_rfq(&self, payload: CancelRfqData) -> Result<(), ManagedWsError> {
245 self.send(ClientMessage::CancelRfq(payload)).await
246 }
247
248 pub async fn accept_quote(&self, payload: AcceptQuoteMessage) -> Result<(), VaultRequestError> {
249 self.command(ClientMessage::AcceptQuote(payload)).await
250 }
251
252 pub fn verify_and_sign_sponsored_tx(
254 &self,
255 to_sign: &SponsoredTxToSignData,
256 intent: &VaultOpenIntent<'_>,
257 tables: &[AddressLookupTableAccount],
258 bounds: ComputeBudgetBounds,
259 ) -> Result<SubmitSignedSponsoredTxData, VaultTxError> {
260 let transaction = verify_vault_open(to_sign, intent, tables, bounds)?;
261 ensure_system_time_ahead("signature_deadline", to_sign.signature_deadline)?;
262 Ok(SubmitSignedSponsoredTxData {
263 request_id: Uuid::new_v4(),
264 order_id: to_sign.order_id,
265 tx_base64: transaction.sign(self.operator.as_ref())?,
266 })
267 }
268
269 pub async fn submit_signed_sponsored_tx(
270 &self,
271 payload: SubmitSignedSponsoredTxData,
272 ) -> Result<(), VaultRequestError> {
273 self.command(ClientMessage::SubmitSignedSponsoredTx(payload))
274 .await
275 }
276
277 pub async fn swap_quote(
278 &self,
279 payload: SwapQuoteData,
280 ) -> Result<SwapQuoteResultData, VaultRequestError> {
281 self.request(
282 ClientMessage::SwapQuote(payload),
283 "swap_quote_result",
284 |message| match message {
285 ServerMessage::SwapQuoteResult(data) => Some(data.clone()),
286 _ => None,
287 },
288 )
289 .await
290 }
291
292 pub fn verify_and_sign_swap(
294 &self,
295 intent: &VaultSwapIntent<'_>,
296 tables: &[AddressLookupTableAccount],
297 bounds: ComputeBudgetBounds,
298 ) -> Result<SwapSubmitData, VaultTxError> {
299 let transaction = verify_vault_swap(intent, tables, bounds)?;
300 ensure_deadline_ahead("valid_until", &intent.quote.valid_until)?;
301 Ok(SwapSubmitData {
302 request_id: Uuid::new_v4(),
303 execution_id: intent.quote.execution_id,
304 signed_tx_base64: transaction.sign(self.operator.as_ref())?,
305 })
306 }
307
308 pub async fn swap_submit_in_epoch(
311 &self,
312 payload: SwapSubmitData,
313 connection_epoch: u64,
314 gate: &impl SendGate,
315 ) -> Result<SwapSubmitResultData, VaultRequestError> {
316 self.gated_request(
317 gate,
318 connection_epoch,
319 ClientMessage::SwapSubmit(payload),
320 "swap_submit_result",
321 |message| match message {
322 ServerMessage::SwapSubmitResult(data) => Some(data.clone()),
323 _ => None,
324 },
325 )
326 .await
327 }
328
329 pub async fn swap_status(
331 &self,
332 execution_id: OrderId,
333 ) -> Result<SwapStatusMessage, VaultRequestError> {
334 self.request(
335 ClientMessage::GetSwapStatus(GetSwapStatusMessage {
336 request_id: Uuid::new_v4(),
337 execution_id,
338 }),
339 "swap_status",
340 |message| match message {
341 ServerMessage::SwapStatus(data) => Some(data.clone()),
342 _ => None,
343 },
344 )
345 .await
346 }
347
348 pub async fn subscribe(&self, payload: SubscribeData) -> Result<(), ManagedWsError> {
349 self.send(ClientMessage::Subscribe(payload)).await
350 }
351
352 pub async fn unsubscribe(&self, payload: UnsubscribeData) -> Result<(), ManagedWsError> {
353 self.send(ClientMessage::Unsubscribe(payload)).await
354 }
355
356 pub fn subscribe_messages(&self) -> ManagedMessageReceiver {
357 self.inner.subscribe_messages()
358 }
359
360 pub fn subscribe_reference_changes(&self) -> ReferenceChangeReceiver {
361 self.inner.subscribe_reference_changes()
362 }
363
364 pub fn subscribe_projections(&self) -> VaultProjectionReceiver {
365 VaultProjectionReceiver::new(self.inner.subscribe_messages())
366 }
367
368 pub fn subscribe_events(&self) -> broadcast::Receiver<ManagedWsEvent> {
369 self.inner.subscribe_events()
370 }
371
372 #[must_use]
373 pub fn state(&self) -> ManagedWsState {
374 self.inner.state()
375 }
376
377 pub async fn wait_until_ready(&self) -> Result<u64, WaitUntilReadyError> {
378 self.inner.wait_until_ready().await
379 }
380
381 #[must_use]
382 pub const fn raw(&self) -> &ManagedWsHandle {
383 &self.inner
384 }
385
386 #[must_use]
387 pub fn into_raw(self) -> ManagedWsHandle {
388 self.inner
389 }
390
391 pub async fn close(&self) -> Result<(), ManagedWsError> {
392 self.inner.close().await
393 }
394
395 async fn send(&self, message: ClientMessage) -> Result<(), ManagedWsError> {
396 self.inner.ensure_ready()?;
397 self.inner.send(message).await
398 }
399
400 async fn command(&self, message: ClientMessage) -> Result<(), VaultRequestError> {
401 self.request(message, "command_accepted", |message| match message {
402 ServerMessage::CommandAccepted(_) => Some(()),
403 _ => None,
404 })
405 .await
406 }
407
408 #[cfg(feature = "chain-rpc")]
409 pub(super) async fn gated_command(
410 &self,
411 gate: &impl SendGate,
412 connection_epoch: u64,
413 message: ClientMessage,
414 ) -> Result<(), VaultRequestError> {
415 self.gated_request(
416 gate,
417 connection_epoch,
418 message,
419 "command_accepted",
420 |message| match message {
421 ServerMessage::CommandAccepted(_) => Some(()),
422 _ => None,
423 },
424 )
425 .await
426 }
427
428 async fn gated_request<T>(
429 &self,
430 gate: &impl SendGate,
431 connection_epoch: u64,
432 message: ClientMessage,
433 expected: &'static str,
434 extract: fn(&ServerMessage) -> Option<T>,
435 ) -> Result<T, VaultRequestError> {
436 let response = gate
437 .admit(connection_epoch, || {
438 self.inner
439 .try_send_await_in_epoch(message, self.request_timeout, connection_epoch)
440 })
441 .ok_or(VaultRequestError::Gated)??
442 .await?;
443 Self::correlated(response, expected, extract)
444 }
445
446 async fn request<T>(
447 &self,
448 message: ClientMessage,
449 expected: &'static str,
450 extract: fn(&ServerMessage) -> Option<T>,
451 ) -> Result<T, VaultRequestError> {
452 self.inner.ensure_ready()?;
453 let response = self.inner.send_await(message, self.request_timeout).await?;
454 Self::correlated(response, expected, extract)
455 }
456
457 fn correlated<T>(
458 response: Arc<ServerMessage>,
459 expected: &'static str,
460 extract: fn(&ServerMessage) -> Option<T>,
461 ) -> Result<T, VaultRequestError> {
462 if let ServerMessage::RequestError(error) = &*response {
463 return Err(VaultRequestError::Rejected {
464 request_id: error.request_id,
465 response,
466 });
467 }
468 extract(&response).ok_or(VaultRequestError::UnexpectedResponse { expected, response })
469 }
470}