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::wire::{WireError, decode_base58_32};
11use crate::ws::managed::{
12 ManagedMessageReceiver, ManagedWsConfigError, ManagedWsError, ManagedWsEvent, ManagedWsHandle,
13 ManagedWsState, ReferenceChangeReceiver, SendAwaitError, SendTicket, WaitUntilReadyError,
14 spawn_managed_ws,
15};
16use crate::ws::types::{
17 AcceptQuoteMessage, BatchQuotesMessage, CancelAllQuotesMessage, CancelQuoteData, CancelRfqData,
18 ClientMessage, QuoteMessage, ReplaceQuoteMessage, RfqRequestMessage, ServerError,
19 ServerMessage, SponsoredTxToSignData, SubmitSignedSponsoredTxData, SubscribeData,
20 SwapQuoteData, SwapQuoteResultData, SwapSubmitData, SwapSubmitResultData, UnsubscribeData,
21 VaultAuthSuccessData,
22};
23
24use solana_sdk::message::AddressLookupTableAccount;
25
26use crate::chain::vault::{
27 ComputeBudgetBounds, VaultOpenIntent, VaultSwapIntent, verify_vault_open, verify_vault_swap,
28};
29
30use super::config::VaultOperatorConfig;
31use super::projection::VaultProjectionReceiver;
32use super::tx::{VaultTxError, ensure_deadline_ahead, ensure_system_time_ahead};
33
34#[derive(Debug, thiserror::Error)]
35#[non_exhaustive]
36pub enum VaultClientError {
37 #[error("vault_pda is not a base58 32-byte address")]
38 InvalidVaultPda(#[source] WireError),
39 #[error(transparent)]
40 InvalidConfig(#[from] ManagedWsConfigError),
41}
42
43#[derive(Debug, thiserror::Error)]
44#[non_exhaustive]
45pub enum VaultRequestError {
46 #[error(transparent)]
47 Session(#[from] ManagedWsError),
48 #[error(transparent)]
49 Transport(#[from] SendAwaitError),
50 #[error("server rejected request {request_id}")]
51 Rejected {
52 request_id: Uuid,
53 response: Arc<ServerMessage>,
54 },
55 #[error("expected {expected}, received a different correlated response")]
56 UnexpectedResponse {
57 expected: &'static str,
58 response: Arc<ServerMessage>,
59 },
60}
61
62impl VaultRequestError {
63 #[must_use]
64 pub fn server_error(&self) -> Option<&ServerError> {
65 match self {
66 Self::Rejected { response, .. } => match &**response {
67 ServerMessage::RequestError(error) => Some(&error.error),
68 _ => None,
69 },
70 _ => None,
71 }
72 }
73}
74
75#[derive(Clone)]
77pub struct VaultOperatorClient {
78 inner: ManagedWsHandle,
79 vault_pda: String,
80 vault_owner: [u8; 32],
81 operator: Arc<dyn SignerLike + Send + Sync>,
82 request_timeout: Duration,
83}
84
85impl VaultOperatorClient {
86 pub fn spawn(config: VaultOperatorConfig) -> Result<Self, VaultClientError> {
87 let vault_owner =
88 decode_base58_32(&config.vault_pda).map_err(VaultClientError::InvalidVaultPda)?;
89 Ok(Self {
90 inner: spawn_managed_ws(config.managed)?,
91 vault_pda: config.vault_pda,
92 vault_owner,
93 operator: config.operator,
94 request_timeout: config.request_timeout,
95 })
96 }
97
98 #[must_use]
99 pub fn vault_pda(&self) -> &str {
100 &self.vault_pda
101 }
102
103 #[must_use]
105 pub const fn vault_owner(&self) -> [u8; 32] {
106 self.vault_owner
107 }
108
109 #[must_use]
110 pub fn operator_pubkey(&self) -> String {
111 self.operator.pubkey_base58()
112 }
113
114 #[must_use]
116 pub fn auth(&self) -> Option<VaultAuthSuccessData> {
117 self.inner.vault_auth()
118 }
119
120 #[must_use]
121 pub fn permissions(&self) -> Option<VaultDelegatePermissions> {
122 self.auth().map(|auth| auth.permissions)
123 }
124
125 #[must_use]
126 pub fn can_maker(&self) -> bool {
127 self.inner.is_ready()
128 && self.auth().is_some_and(|auth| {
129 matches!(
130 auth.operator_kind,
131 crate::types::VaultOperatorKind::Delegate
132 | crate::types::VaultOperatorKind::Governance
133 ) && auth.permissions.can_maker()
134 })
135 }
136
137 #[must_use]
138 pub fn can_taker(&self) -> bool {
139 self.inner.is_ready()
140 && self
141 .permissions()
142 .is_some_and(VaultDelegatePermissions::can_taker)
143 }
144
145 #[must_use]
146 pub fn subscribe_vault_auth(&self) -> watch::Receiver<Option<VaultAuthSuccessData>> {
147 self.inner.subscribe_vault_auth()
148 }
149
150 pub fn sign_quote(&self, quote: &QuoteBuilder<'_>) -> Result<QuoteMessage, QuoteBuildError> {
151 quote
152 .clone()
153 .maker_owner(self.vault_owner)
154 .sign(self.operator.as_ref())
155 }
156
157 pub async fn quote(&self, payload: QuoteMessage) -> Result<(), ManagedWsError> {
158 self.send(ClientMessage::Quote(payload)).await
159 }
160
161 pub fn try_quote(&self, payload: QuoteMessage) -> Result<SendTicket, ManagedWsError> {
162 self.inner.ensure_ready()?;
163 self.inner.try_send(ClientMessage::Quote(payload))
164 }
165
166 pub async fn replace_quote(&self, payload: ReplaceQuoteMessage) -> Result<(), ManagedWsError> {
167 self.send(ClientMessage::ReplaceQuote(payload)).await
168 }
169
170 pub async fn batch_quotes(&self, payload: BatchQuotesMessage) -> Result<(), ManagedWsError> {
171 self.send(ClientMessage::BatchQuotes(payload)).await
172 }
173
174 pub async fn cancel_quote(&self, payload: CancelQuoteData) -> Result<(), ManagedWsError> {
175 self.send(ClientMessage::CancelQuote(payload)).await
176 }
177
178 pub async fn cancel_all_quotes(
179 &self,
180 payload: CancelAllQuotesMessage,
181 ) -> Result<(), ManagedWsError> {
182 self.send(ClientMessage::CancelAllQuotes(payload)).await
183 }
184
185 pub async fn rfq_request(&self, payload: RfqRequestMessage) -> Result<(), ManagedWsError> {
186 self.send(ClientMessage::RfqRequest(payload)).await
187 }
188
189 pub async fn cancel_rfq(&self, payload: CancelRfqData) -> Result<(), ManagedWsError> {
190 self.send(ClientMessage::CancelRfq(payload)).await
191 }
192
193 pub async fn accept_quote(&self, payload: AcceptQuoteMessage) -> Result<(), ManagedWsError> {
194 self.send(ClientMessage::AcceptQuote(payload)).await
195 }
196
197 pub fn verify_and_sign_sponsored_tx(
199 &self,
200 to_sign: &SponsoredTxToSignData,
201 intent: &VaultOpenIntent<'_>,
202 tables: &[AddressLookupTableAccount],
203 bounds: ComputeBudgetBounds,
204 ) -> Result<SubmitSignedSponsoredTxData, VaultTxError> {
205 let transaction = verify_vault_open(to_sign, intent, tables, bounds)?;
206 ensure_system_time_ahead("signature_deadline", to_sign.signature_deadline)?;
207 Ok(SubmitSignedSponsoredTxData {
208 order_id: to_sign.order_id,
209 tx_base64: transaction.sign(self.operator.as_ref())?,
210 })
211 }
212
213 pub async fn submit_signed_sponsored_tx(
214 &self,
215 payload: SubmitSignedSponsoredTxData,
216 ) -> Result<(), ManagedWsError> {
217 self.send(ClientMessage::SubmitSignedSponsoredTx(payload))
218 .await
219 }
220
221 pub async fn swap_quote(
222 &self,
223 payload: SwapQuoteData,
224 ) -> Result<SwapQuoteResultData, VaultRequestError> {
225 self.request(
226 ClientMessage::SwapQuote(payload),
227 "swap_quote_result",
228 |message| match message {
229 ServerMessage::SwapQuoteResult(data) => Ok(data),
230 other => Err(other),
231 },
232 )
233 .await
234 }
235
236 pub fn verify_and_sign_swap(
238 &self,
239 intent: &VaultSwapIntent<'_>,
240 tables: &[AddressLookupTableAccount],
241 bounds: ComputeBudgetBounds,
242 ) -> Result<SwapSubmitData, VaultTxError> {
243 let transaction = verify_vault_swap(intent, tables, bounds)?;
244 ensure_deadline_ahead("valid_until", &intent.quote.valid_until)?;
245 Ok(SwapSubmitData {
246 request_id: Uuid::new_v4(),
247 execution_id: intent.quote.execution_id,
248 signed_tx_base64: transaction.sign(self.operator.as_ref())?,
249 })
250 }
251
252 pub async fn swap_submit(
253 &self,
254 payload: SwapSubmitData,
255 ) -> Result<SwapSubmitResultData, VaultRequestError> {
256 self.request(
257 ClientMessage::SwapSubmit(payload),
258 "swap_submit_result",
259 |message| match message {
260 ServerMessage::SwapSubmitResult(data) => Ok(data),
261 other => Err(other),
262 },
263 )
264 .await
265 }
266
267 pub async fn subscribe(&self, payload: SubscribeData) -> Result<(), ManagedWsError> {
268 self.send(ClientMessage::Subscribe(payload)).await
269 }
270
271 pub async fn unsubscribe(&self, payload: UnsubscribeData) -> Result<(), ManagedWsError> {
272 self.send(ClientMessage::Unsubscribe(payload)).await
273 }
274
275 pub fn subscribe_messages(&self) -> ManagedMessageReceiver {
276 self.inner.subscribe_messages()
277 }
278
279 pub fn subscribe_reference_changes(&self) -> ReferenceChangeReceiver {
280 self.inner.subscribe_reference_changes()
281 }
282
283 pub fn subscribe_projections(&self) -> VaultProjectionReceiver {
284 VaultProjectionReceiver::new(self.inner.subscribe_messages())
285 }
286
287 pub fn subscribe_events(&self) -> broadcast::Receiver<ManagedWsEvent> {
288 self.inner.subscribe_events()
289 }
290
291 #[must_use]
292 pub fn state(&self) -> ManagedWsState {
293 self.inner.state()
294 }
295
296 pub async fn wait_until_ready(&self) -> Result<u64, WaitUntilReadyError> {
297 self.inner.wait_until_ready().await
298 }
299
300 #[must_use]
301 pub const fn raw(&self) -> &ManagedWsHandle {
302 &self.inner
303 }
304
305 #[must_use]
306 pub fn into_raw(self) -> ManagedWsHandle {
307 self.inner
308 }
309
310 pub async fn close(&self) -> Result<(), ManagedWsError> {
311 self.inner.close().await
312 }
313
314 async fn send(&self, message: ClientMessage) -> Result<(), ManagedWsError> {
315 self.inner.ensure_ready()?;
316 self.inner.send(message).await
317 }
318
319 async fn request<T>(
320 &self,
321 message: ClientMessage,
322 expected: &'static str,
323 extract: fn(ServerMessage) -> Result<T, ServerMessage>,
324 ) -> Result<T, VaultRequestError> {
325 self.inner.ensure_ready()?;
326 let response = self.inner.send_await(message, self.request_timeout).await?;
327 if let ServerMessage::RequestError(error) = &*response {
328 return Err(VaultRequestError::Rejected {
329 request_id: error.request_id,
330 response,
331 });
332 }
333 let owned = match Arc::try_unwrap(response) {
334 Ok(owned) => owned,
335 Err(shared) => (*shared).clone(),
336 };
337 extract(owned).map_err(|other| VaultRequestError::UnexpectedResponse {
338 expected,
339 response: Arc::new(other),
340 })
341 }
342}