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