1use std::future::Future;
2use std::time::Duration;
3
4use solana_sdk::pubkey::Pubkey;
5use uuid::Uuid;
6
7use crate::chain::vault::{
8 ComputeBudgetBounds, Counterparty, VaultManager, VaultSaleTerms, VaultSpreadTerms,
9 VaultTradeTerms,
10};
11use crate::chain::{ChainClient, ChainError, MarketInfo};
12use crate::spreads::{SpreadError, SpreadType};
13use crate::types::ids::OrderId;
14use crate::types::{OptionTerms, Quantity, Strike, TakerSide, TimeoutSeconds};
15use crate::ws::managed::ManagedReceiveError;
16use crate::ws::types::{
17 AcceptQuoteMessage, ClientMessage, Delivery, QuoteReceivedMessage, RfqRequestMessage,
18 ServerMessage, SponsoredTxToSignData,
19};
20
21use super::client::{VaultOperatorClient, VaultRequestError};
22use super::gate::SendGate;
23use super::tx::VaultTxError;
24
25#[derive(Debug, Clone)]
27pub struct VaultSale {
28 pub market_pda: Pubkey,
29 pub market: MarketInfo,
30 pub terms: OptionTerms,
32 pub quantity: Quantity,
33 pub rfq_timeout: TimeoutSeconds,
34 pub window: Duration,
37 pub bounds: ComputeBudgetBounds,
38}
39
40impl VaultSale {
41 pub(super) fn signed_terms(
43 &self,
44 quote: &QuoteReceivedMessage,
45 ) -> Result<VaultSaleTerms, ChainError> {
46 let market = &self.market;
47 let spread = |kind, lower_strike: Strike, upper_strike: Strike| {
48 VaultSaleTerms::Spread(VaultSpreadTerms {
49 underlying_mint: market.underlying_mint,
50 quote_mint: market.quote_mint,
51 expiry_ts: market.expiry_ts,
52 kind,
53 side: quote.side,
54 lower_strike: lower_strike.value(),
55 upper_strike: upper_strike.value(),
56 quantity: self.quantity.value(),
57 price: quote.price.value(),
58 valid_until: quote.valid_until.as_unix_seconds(),
59 nonce: quote.nonce.value(),
60 })
61 };
62 Ok(match self.terms {
63 OptionTerms::CashPut { strike } => {
64 spread(SpreadType::PutSpread, Strike::new(0), strike)
65 }
66 OptionTerms::CoveredCall { strike } | OptionTerms::CashSecuredPut { strike } => {
67 VaultSaleTerms::Physical(VaultTradeTerms {
68 underlying_mint: market.underlying_mint,
69 quote_mint: market.quote_mint,
70 expiry_ts: market.expiry_ts,
71 is_put: market.is_put,
72 side: quote.side,
73 strike: strike.value(),
74 quantity: self.quantity.value(),
75 price: quote.price.value(),
76 valid_until: quote.valid_until.as_unix_seconds(),
77 nonce: quote.nonce.value(),
78 })
79 }
80 OptionTerms::CallSpread {
81 lower_strike,
82 upper_strike,
83 } => spread(SpreadType::CallSpread, lower_strike, upper_strike),
84 OptionTerms::PutSpread {
85 lower_strike,
86 upper_strike,
87 } => spread(SpreadType::PutSpread, lower_strike, upper_strike),
88 })
89 }
90}
91
92pub trait SaleDesk: Send {
94 type Error: std::fmt::Debug + std::fmt::Display + Send + Sync + 'static;
95
96 fn take(
98 &mut self,
99 quote: &QuoteReceivedMessage,
100 ) -> impl Future<Output = Result<bool, Self::Error>> + Send;
101
102 fn accepting(&mut self, _quote: &QuoteReceivedMessage) -> Result<(), Self::Error> {
104 Ok(())
105 }
106
107 fn writer(&mut self) -> impl Future<Output = Result<VaultManager, Self::Error>> + Send;
109
110 fn signed(&mut self, _sale: &SignedSale) -> Result<(), Self::Error> {
112 Ok(())
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct SignedSale {
122 pub order_id: OrderId,
123 pub participant_signature: String,
125 pub last_valid_block_height: u64,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum SaleOutcome {
130 Unfilled,
132 Submitted(SignedSale),
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum SaleStage {
139 Request,
140 Take,
141 Accept,
142 Sign,
143}
144
145#[derive(Debug, thiserror::Error)]
146#[non_exhaustive]
147pub enum SaleError<E> {
148 #[error("market_pda is not the program's market for these terms")]
149 Market,
150 #[error("spread terms: {0}")]
151 Spread(SpreadError),
152 #[error("sale desk: {0}")]
153 Desk(E),
154 #[error("{stage:?} failed")]
155 Rejected {
156 stage: SaleStage,
157 #[source]
158 error: VaultRequestError,
159 },
160 #[error(transparent)]
161 Stream(#[from] ManagedReceiveError),
162 #[error("the session changed during the sale")]
163 SessionChanged,
164 #[error("the accepted quote was not served within the window")]
165 Unserved,
166 #[error("{0:?} did not finish within the window")]
168 TimedOut(SaleStage),
169 #[error(transparent)]
170 Chain(#[from] ChainError),
171 #[error(transparent)]
172 Sign(#[from] VaultTxError),
173 #[error("submitting the signed sale failed")]
174 Submit {
175 sale: SignedSale,
176 #[source]
177 error: VaultRequestError,
178 },
179}
180
181impl<E> SaleError<E> {
182 #[must_use]
185 pub fn in_flight(&self) -> Option<&SignedSale> {
186 match self {
187 Self::Submit { sale, error } if error.delivery() != Some(Delivery::NotForwarded) => {
188 Some(sale)
189 }
190 _ => None,
191 }
192 }
193}
194
195impl VaultOperatorClient {
196 pub async fn sell_rfq<D: SaleDesk>(
201 &self,
202 chain: &ChainClient,
203 sale: &VaultSale,
204 connection_epoch: u64,
205 gate: &(impl SendGate + Sync),
206 desk: &mut D,
207 ) -> Result<SaleOutcome, SaleError<D::Error>> {
208 let market = &sale.market;
209 let canonical = crate::quote::market_pda(
210 &chain.program_id(),
211 &market.underlying_mint,
212 &market.quote_mint,
213 market.expiry_ts,
214 market.is_put,
215 );
216 if canonical != sale.market_pda || sale.terms.position_type().is_put() != market.is_put {
217 return Err(SaleError::Market);
218 }
219 if let Some(upper) = sale.terms.upper_strike() {
220 let product = sale.terms.position_type();
221 if product != crate::PositionType::CashPut && sale.terms.strike().value() == 0 {
222 return Err(SaleError::Spread(SpreadError::InvalidStrikes));
223 }
224 crate::spreads::validate_strikes(
225 SpreadType::try_from(u8::from(sale.terms.to_chain().position_type()))
226 .map_err(SaleError::Spread)?,
227 sale.terms.to_chain().strike().value(),
228 upper.value(),
229 )
230 .map_err(SaleError::Spread)?;
231 }
232 let mut messages = self.subscribe_messages();
233 let request_id = Uuid::new_v4();
234 self.gated_command(
235 gate,
236 connection_epoch,
237 ClientMessage::RfqRequest(RfqRequestMessage {
238 request_id,
239 market: crate::MarketId::new(crate::Address32::new(sale.market_pda.to_bytes())),
240 terms: sale.terms,
241 side: TakerSide::Sell,
242 quantity: sale.quantity,
243 timeout_seconds: sale.rfq_timeout,
244 }),
245 )
246 .await
247 .map_err(|error| SaleError::Rejected {
248 stage: SaleStage::Request,
249 error,
250 })?;
251
252 let mut rfq_id = None;
253 let mut taken: Option<QuoteReceivedMessage> = None;
254 let mut deadline = tokio::time::Instant::now() + sale.window;
255 loop {
256 let Ok(inbound) = tokio::time::timeout_at(deadline, messages.recv()).await else {
257 return match taken {
258 None => Ok(SaleOutcome::Unfilled),
259 Some(_) => Err(SaleError::Unserved),
260 };
261 };
262 let inbound = inbound?;
263 if inbound.connection_epoch != connection_epoch {
264 return Err(SaleError::SessionChanged);
265 }
266 match inbound.message() {
267 ServerMessage::RfqCreated(created) if created.request_id == Some(request_id) => {
268 rfq_id = Some(created.rfq_id);
269 }
270 ServerMessage::QuoteReceived(quote) if taken.is_none() => {
271 if Some(quote.rfq_id) == rfq_id
272 && quote.terms == sale.terms
273 && quote.side == TakerSide::Sell
274 && tokio::time::timeout_at(deadline, desk.take(quote))
275 .await
276 .map_err(|_| SaleError::TimedOut(SaleStage::Take))?
277 .map_err(SaleError::Desk)?
278 {
279 desk.accepting(quote).map_err(SaleError::Desk)?;
280 self.gated_command(
281 gate,
282 connection_epoch,
283 ClientMessage::AcceptQuote(AcceptQuoteMessage {
284 request_id: Uuid::new_v4(),
285 rfq_id: quote.rfq_id,
286 maker: quote.maker,
287 order_id: quote.order_id,
288 }),
289 )
290 .await
291 .map_err(|error| SaleError::Rejected {
292 stage: SaleStage::Accept,
293 error,
294 })?;
295 taken = Some(quote.clone());
296 deadline = tokio::time::Instant::now() + sale.window;
297 }
298 }
299 ServerMessage::SponsoredTxToSign(served) => {
300 if let Some(quote) = taken.as_ref().filter(|q| q.order_id == served.order_id) {
301 let signed = tokio::time::timeout_at(
302 deadline,
303 self.sign_sale(chain, sale, quote, served, desk),
304 )
305 .await
306 .map_err(|_| SaleError::TimedOut(SaleStage::Sign))??;
307 return self.submit_sale(gate, connection_epoch, signed, desk).await;
308 }
309 }
310 _ => {}
311 }
312 }
313 }
314
315 async fn sign_sale<D: SaleDesk>(
316 &self,
317 chain: &ChainClient,
318 sale: &VaultSale,
319 quote: &QuoteReceivedMessage,
320 served: &SponsoredTxToSignData,
321 desk: &mut D,
322 ) -> Result<(SignedSale, crate::ws::types::SubmitSignedSponsoredTxData), SaleError<D::Error>>
323 {
324 let writer = desk.writer().await.map_err(SaleError::Desk)?;
325 let maker = Pubkey::from(quote.maker);
326 let counterparty = Counterparty::load(chain, maker).await?;
327 let keeper = chain.hot_authority().await?;
328 let tables = chain
329 .address_lookup_tables(
330 &crate::chain::vault::served_lookup_table_keys(&served.tx_base64)
331 .map_err(VaultTxError::from)?,
332 )
333 .await?;
334 let (submit, participant_signature) = self
335 .verify_and_sign_sponsored_tx_with_participant_signature(
336 served,
337 &counterparty.sale(&writer, keeper, sale.signed_terms(quote)?),
338 &tables,
339 sale.bounds,
340 )?;
341 let signed = SignedSale {
342 order_id: served.order_id,
343 participant_signature,
344 last_valid_block_height: served.last_valid_block_height,
345 };
346 Ok((signed, submit))
347 }
348
349 async fn submit_sale<D: SaleDesk>(
350 &self,
351 gate: &(impl SendGate + Sync),
352 connection_epoch: u64,
353 (signed, submit): (SignedSale, crate::ws::types::SubmitSignedSponsoredTxData),
354 desk: &mut D,
355 ) -> Result<SaleOutcome, SaleError<D::Error>> {
356 desk.signed(&signed).map_err(SaleError::Desk)?;
357 match self
358 .gated_command(
359 gate,
360 connection_epoch,
361 ClientMessage::SubmitSignedSponsoredTx(submit),
362 )
363 .await
364 {
365 Ok(()) => Ok(SaleOutcome::Submitted(signed)),
366 Err(error) => Err(SaleError::Submit {
367 sale: signed,
368 error,
369 }),
370 }
371 }
372}