1use std::future::Future;
2use std::time::Duration;
3
4use base64::Engine as _;
5use base64::engine::general_purpose::STANDARD;
6use solana_sdk::pubkey::Pubkey;
7use uuid::Uuid;
8
9use crate::chain::vault::{ComputeBudgetBounds, Counterparty, VaultManager, VaultTradeTerms};
10use crate::chain::{ChainClient, ChainError, MarketInfo};
11use crate::types::ids::OrderId;
12use crate::types::{PositionType, Quantity, Strike, TimeoutSeconds};
13use crate::ws::managed::ManagedReceiveError;
14use crate::ws::types::{
15 AcceptQuoteMessage, ClientMessage, Delivery, QuoteReceivedMessage, RfqRequestMessage,
16 ServerMessage, SponsoredTxToSignData,
17};
18
19use super::client::{VaultOperatorClient, VaultRequestError};
20use super::gate::SendGate;
21use super::tx::VaultTxError;
22
23#[derive(Debug, Clone)]
25pub struct VaultSale {
26 pub market_pda: Pubkey,
27 pub market: MarketInfo,
28 pub strike: Strike,
29 pub quantity: Quantity,
30 pub rfq_timeout: TimeoutSeconds,
31 pub window: Duration,
34 pub bounds: ComputeBudgetBounds,
35}
36
37pub trait SaleDesk: Send {
39 type Error: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static;
40
41 fn take(
43 &mut self,
44 quote: &QuoteReceivedMessage,
45 ) -> impl Future<Output = Result<bool, Self::Error>> + Send;
46
47 fn accepting(&mut self, _quote: &QuoteReceivedMessage) -> Result<(), Self::Error> {
49 Ok(())
50 }
51
52 fn writer(&mut self) -> impl Future<Output = Result<VaultManager, Self::Error>> + Send;
54
55 fn signed(&mut self, _sale: &SignedSale) -> Result<(), Self::Error> {
57 Ok(())
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct SignedSale {
64 pub order_id: OrderId,
65 pub signature: String,
66 pub last_valid_block_height: u64,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub enum SaleOutcome {
71 Unfilled,
73 Submitted(SignedSale),
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum SaleStage {
79 Request,
80 Take,
81 Accept,
82 Sign,
83}
84
85#[derive(Debug, thiserror::Error)]
86#[non_exhaustive]
87pub enum SaleError<E> {
88 #[error("market_pda is not the program's market for these terms")]
89 Market,
90 #[error("sale desk: {0}")]
91 Desk(E),
92 #[error("{stage:?} failed")]
93 Rejected {
94 stage: SaleStage,
95 #[source]
96 error: VaultRequestError,
97 },
98 #[error(transparent)]
99 Stream(#[from] ManagedReceiveError),
100 #[error("the session changed during the sale")]
101 SessionChanged,
102 #[error("the accepted quote was not served within the window")]
103 Unserved,
104 #[error("{0:?} did not finish within the window")]
106 TimedOut(SaleStage),
107 #[error(transparent)]
108 Chain(#[from] ChainError),
109 #[error(transparent)]
110 Sign(#[from] VaultTxError),
111 #[error("submitting the signed sale failed")]
112 Submit {
113 sale: SignedSale,
114 #[source]
115 error: VaultRequestError,
116 },
117}
118
119impl<E> SaleError<E> {
120 #[must_use]
123 pub fn in_flight(&self) -> Option<&SignedSale> {
124 match self {
125 Self::Submit { sale, error } if error.delivery() != Some(Delivery::NotForwarded) => {
126 Some(sale)
127 }
128 _ => None,
129 }
130 }
131}
132
133impl VaultOperatorClient {
134 pub async fn sell_rfq<D: SaleDesk>(
139 &self,
140 chain: &ChainClient,
141 sale: &VaultSale,
142 connection_epoch: u64,
143 gate: &(impl SendGate + Sync),
144 desk: &mut D,
145 ) -> Result<SaleOutcome, SaleError<D::Error>> {
146 let market = &sale.market;
147 let canonical = crate::quote::market_pda(
148 &chain.program_id(),
149 &market.underlying_mint,
150 &market.quote_mint,
151 market.expiry_ts,
152 market.is_put,
153 );
154 if canonical != sale.market_pda {
155 return Err(SaleError::Market);
156 }
157 let mut messages = self.subscribe_messages();
158 let request_id = Uuid::new_v4();
159 self.gated_command(
160 gate,
161 connection_epoch,
162 ClientMessage::RfqRequest(RfqRequestMessage {
163 request_id,
164 market: sale.market_pda.to_string().into(),
165 position_type: if market.is_put {
166 PositionType::CashSecuredPut
167 } else {
168 PositionType::CoveredCall
169 },
170 strike: sale.strike,
171 quantity: sale.quantity,
172 timeout_seconds: sale.rfq_timeout,
173 }),
174 )
175 .await
176 .map_err(|error| SaleError::Rejected {
177 stage: SaleStage::Request,
178 error,
179 })?;
180
181 let mut rfq_id = None;
182 let mut taken: Option<QuoteReceivedMessage> = None;
183 let mut deadline = tokio::time::Instant::now() + sale.window;
184 loop {
185 let Ok(inbound) = tokio::time::timeout_at(deadline, messages.recv()).await else {
186 return match taken {
187 None => Ok(SaleOutcome::Unfilled),
188 Some(_) => Err(SaleError::Unserved),
189 };
190 };
191 let inbound = inbound?;
192 if inbound.connection_epoch != connection_epoch {
193 return Err(SaleError::SessionChanged);
194 }
195 match inbound.message() {
196 ServerMessage::RfqCreated(created) if created.request_id == Some(request_id) => {
197 rfq_id = Some(created.rfq_id);
198 }
199 ServerMessage::QuoteReceived(quote) if taken.is_none() => {
200 if Some(quote.rfq_id) == rfq_id
201 && quote.strike == sale.strike
202 && tokio::time::timeout_at(deadline, desk.take(quote))
203 .await
204 .map_err(|_| SaleError::TimedOut(SaleStage::Take))?
205 .map_err(SaleError::Desk)?
206 {
207 desk.accepting(quote).map_err(SaleError::Desk)?;
208 self.gated_command(
209 gate,
210 connection_epoch,
211 ClientMessage::AcceptQuote(AcceptQuoteMessage {
212 request_id: Uuid::new_v4(),
213 rfq_id: quote.rfq_id,
214 maker: quote.maker.clone(),
215 order_id: quote.order_id,
216 }),
217 )
218 .await
219 .map_err(|error| SaleError::Rejected {
220 stage: SaleStage::Accept,
221 error,
222 })?;
223 taken = Some(quote.clone());
224 deadline = tokio::time::Instant::now() + sale.window;
225 }
226 }
227 ServerMessage::SponsoredTxToSign(served) => {
228 if let Some(quote) = taken.as_ref().filter(|q| q.order_id == served.order_id) {
229 let signed = tokio::time::timeout_at(
230 deadline,
231 self.sign_sale(chain, sale, quote, served, desk),
232 )
233 .await
234 .map_err(|_| SaleError::TimedOut(SaleStage::Sign))??;
235 return self.submit_sale(gate, connection_epoch, signed, desk).await;
236 }
237 }
238 _ => {}
239 }
240 }
241 }
242
243 async fn sign_sale<D: SaleDesk>(
244 &self,
245 chain: &ChainClient,
246 sale: &VaultSale,
247 quote: &QuoteReceivedMessage,
248 served: &SponsoredTxToSignData,
249 desk: &mut D,
250 ) -> Result<(SignedSale, crate::ws::types::SubmitSignedSponsoredTxData), SaleError<D::Error>>
251 {
252 let writer = desk.writer().await.map_err(SaleError::Desk)?;
253 let maker = quote
254 .maker
255 .parse::<Pubkey>()
256 .map_err(|_| ChainError::Identity("quote maker"))?;
257 let counterparty = Counterparty::load(chain, maker).await?;
258 let terms = VaultTradeTerms {
259 underlying_mint: sale.market.underlying_mint,
260 quote_mint: sale.market.quote_mint,
261 expiry_ts: sale.market.expiry_ts,
262 is_put: sale.market.is_put,
263 strike: sale.strike.value(),
264 quantity: sale.quantity.value(),
265 price: quote.price.value(),
266 valid_until: quote.valid_until.as_unix_seconds(),
267 nonce: quote.nonce.value(),
268 };
269 let tables = chain
270 .address_lookup_tables(
271 &crate::chain::vault::served_lookup_table_keys(&served.tx_base64)
272 .map_err(VaultTxError::from)?,
273 )
274 .await?;
275 let submit = self.verify_and_sign_sponsored_tx(
276 served,
277 &counterparty.sale(&writer, terms),
278 &tables,
279 sale.bounds,
280 )?;
281 let signed = SignedSale {
282 order_id: served.order_id,
283 signature: transaction_id(&submit.tx_base64)?,
284 last_valid_block_height: served.last_valid_block_height,
285 };
286 Ok((signed, submit))
287 }
288
289 async fn submit_sale<D: SaleDesk>(
290 &self,
291 gate: &(impl SendGate + Sync),
292 connection_epoch: u64,
293 (signed, submit): (SignedSale, crate::ws::types::SubmitSignedSponsoredTxData),
294 desk: &mut D,
295 ) -> Result<SaleOutcome, SaleError<D::Error>> {
296 desk.signed(&signed).map_err(SaleError::Desk)?;
297 match self
298 .gated_command(
299 gate,
300 connection_epoch,
301 ClientMessage::SubmitSignedSponsoredTx(submit),
302 )
303 .await
304 {
305 Ok(()) => Ok(SaleOutcome::Submitted(signed)),
306 Err(error) => Err(SaleError::Submit {
307 sale: signed,
308 error,
309 }),
310 }
311 }
312}
313
314fn transaction_id(tx_base64: &str) -> Result<String, VaultTxError> {
316 let malformed = || {
317 VaultTxError::Intent(crate::chain::vault::VaultIntentError::Malformed(
318 "signed transaction",
319 ))
320 };
321 let bytes = STANDARD.decode(tx_base64).map_err(|_| malformed())?;
322 match bytes.first() {
323 Some(count) if (1..0x80).contains(count) => bytes
324 .get(1..65)
325 .map(crate::wire::encode_base58)
326 .ok_or_else(malformed),
327 _ => Err(malformed()),
328 }
329}