1use crate::orders::{OrderPreimageArgs, SignerLike, compute_order_id, sign_order_id_with_signer};
6use crate::signing::{AsyncSignerLike, SigningError};
7use crate::types::ids::{Nonce, OrderId, Price, Quantity};
8use crate::types::{OptionTerms, PositionType, QuoteExpiry, TakerSide};
9use crate::wire::{WireError, encode_base58};
10use crate::ws::types::{QuoteMessage, ReplaceQuoteMessage, RfqBroadcastMessage};
11use std::time::Duration;
12use uuid::Uuid;
13
14#[derive(Debug, thiserror::Error)]
16#[non_exhaustive]
17pub enum QuoteBuildError {
18 #[error(transparent)]
19 Order(#[from] crate::orders::OrderError),
20 #[error(transparent)]
21 Spread(#[from] crate::spreads::SpreadError),
22 #[error("rfq field `{field}` is not a valid base58 32-byte key: {source}")]
23 Identity {
24 field: &'static str,
25 #[source]
26 source: WireError,
27 },
28
29 #[error("rfq market does not match trusted signing terms: {0}")]
30 MarketBinding(&'static str),
31
32 #[error("terms {requested:?} are not offered by this rfq; offered: {offered:?}")]
34 TermsNotOffered {
35 requested: OptionTerms,
36 offered: Vec<OptionTerms>,
37 },
38
39 #[error("quote is missing `{0}`")]
40 Missing(&'static str),
41
42 #[error("valid_until {valid_until} is after market expiry {market_expiry}")]
43 ValidUntilAfterMarketExpiry {
44 valid_until: QuoteExpiry,
45 market_expiry: QuoteExpiry,
46 },
47
48 #[error("valid_until {valid_until} is before the venue minimum {min_valid_until}")]
49 ValidUntilTooSoon {
50 valid_until: QuoteExpiry,
51 min_valid_until: QuoteExpiry,
52 },
53
54 #[error("valid_until {valid_until} is within the settlement buffer ending at {buffer_end}")]
57 ValidUntilWithinSettlementBuffer {
58 valid_until: QuoteExpiry,
59 buffer_end: QuoteExpiry,
60 },
61
62 #[error("signing failed: {0}")]
63 Signing(#[from] SigningError),
64}
65
66#[derive(Debug, Clone)]
70pub struct RfqBinding {
71 rfq_id: Uuid,
72 chain_id: u64,
73 program_id: [u8; 32],
74 market: [u8; 32],
75 market_expiry: QuoteExpiry,
76 taker: [u8; 32],
77 terms: OptionTerms,
78 quantity: Quantity,
79 offered_terms: Vec<OptionTerms>,
80 side: TakerSide,
81}
82
83impl RfqBinding {
84 pub fn from_broadcast(rfq: &RfqBroadcastMessage) -> Result<Self, QuoteBuildError> {
90 Self::from_broadcast_for_program(rfq, crate::ACTA_PROGRAM_ID)
91 }
92
93 pub fn from_broadcast_for_program(
98 rfq: &RfqBroadcastMessage,
99 expected_program: [u8; 32],
100 ) -> Result<Self, QuoteBuildError> {
101 let side = rfq.side;
102 let terms = rfq.terms;
103 if terms.is_spread() {
104 let upper = terms
105 .upper_strike()
106 .ok_or(QuoteBuildError::MarketBinding("spread terms"))?;
107 if terms.position_type() != PositionType::CashPut && terms.strike().value() == 0 {
108 return Err(crate::spreads::SpreadError::InvalidStrikes.into());
109 }
110 crate::spreads::validate_strikes(
111 crate::spreads::SpreadType::try_from(u8::from(terms.to_chain().position_type()))?,
112 terms.to_chain().strike().value(),
113 upper.value(),
114 )?;
115 if rfq.quantity.value() == 0
116 || rfq.order_options.len() != 1
117 || rfq.order_options[0] != terms
118 {
119 return Err(QuoteBuildError::MarketBinding("spread pair"));
120 }
121 } else if rfq
122 .order_options
123 .iter()
124 .any(|option| option.is_spread() || option.position_type() != terms.position_type())
125 {
126 return Err(QuoteBuildError::MarketBinding("physical order_options"));
127 }
128 let (market, market_expiry) = validate_market_binding(rfq, expected_program)?;
129 let mut offered_terms = Vec::with_capacity(1 + rfq.order_options.len());
130 offered_terms.push(terms);
131 offered_terms.extend(
132 rfq.order_options
133 .iter()
134 .copied()
135 .filter(|option| *option != terms),
136 );
137
138 Ok(Self {
139 rfq_id: rfq.rfq_id,
140 chain_id: rfq.market.chain_id.value(),
141 program_id: expected_program,
142 market,
143 market_expiry,
144 taker: rfq.taker.0,
145 terms,
146 quantity: rfq.quantity,
147 offered_terms,
148 side,
149 })
150 }
151
152 #[must_use]
153 pub const fn rfq_id(&self) -> Uuid {
154 self.rfq_id
155 }
156
157 #[must_use]
159 pub const fn quantity(&self) -> Quantity {
160 self.quantity
161 }
162
163 #[must_use]
164 pub const fn position_type(&self) -> PositionType {
165 self.terms.position_type()
166 }
167
168 #[must_use]
170 pub const fn terms(&self) -> OptionTerms {
171 self.terms
172 }
173
174 #[must_use]
175 pub const fn side(&self) -> TakerSide {
176 self.side
177 }
178
179 #[must_use]
181 pub const fn market_expiry(&self) -> QuoteExpiry {
182 self.market_expiry
183 }
184
185 #[must_use]
187 pub fn offered_terms(&self) -> &[OptionTerms] {
188 &self.offered_terms
189 }
190
191 pub const fn quote(&self) -> QuoteBuilder<'_> {
193 QuoteBuilder {
194 binding: self,
195 terms: self.terms,
196 price: None,
197 valid_until: None,
198 min_valid_until: None,
199 buffer_end: None,
200 nonce: None,
201 maker: None,
202 }
203 }
204}
205
206fn validate_market_binding(
207 rfq: &RfqBroadcastMessage,
208 expected_program: [u8; 32],
209) -> Result<([u8; 32], QuoteExpiry), QuoteBuildError> {
210 use QuoteBuildError::MarketBinding;
211 use solana_pubkey::Pubkey;
212
213 let descriptor = &rfq.market;
214 if descriptor.chain_id.value() != 0 {
215 return Err(MarketBinding("chain_id"));
216 }
217 if descriptor.program_id.0 != expected_program {
218 return Err(MarketBinding("program_id"));
219 }
220 if descriptor.is_put != rfq.terms.position_type().is_put() {
221 return Err(MarketBinding("terms"));
222 }
223 let underlying = descriptor.underlying_mint.0;
224 let quote = descriptor.quote_mint.0;
225 let (collateral, settlement) = if rfq.terms.is_spread() {
226 (quote, quote)
227 } else if descriptor.is_put {
228 (quote, underlying)
229 } else {
230 (underlying, quote)
231 };
232 if descriptor.collateral_mint.0 != collateral || descriptor.settlement_mint.0 != settlement {
233 return Err(MarketBinding("collateral/settlement mint"));
234 }
235 let expiry = descriptor
236 .expiry_ts
237 .duration_since(std::time::UNIX_EPOCH)
238 .map_err(|_| MarketBinding("expiry_ts"))?;
239 if expiry.subsec_nanos() != 0 || expiry.as_secs() > i64::MAX as u64 {
240 return Err(MarketBinding("expiry_ts"));
241 }
242 let expected_market = market_pda(
243 &Pubkey::new_from_array(expected_program),
244 &Pubkey::new_from_array(underlying),
245 &Pubkey::new_from_array(quote),
246 expiry.as_secs(),
247 descriptor.is_put,
248 )
249 .to_bytes();
250 let market = descriptor.market_pda.0;
251 if market != expected_market {
252 return Err(MarketBinding("market_pda"));
253 }
254 Ok((market, QuoteExpiry::from_unix_seconds(expiry.as_secs())))
255}
256
257pub(crate) fn market_pda(
258 program_id: &solana_pubkey::Pubkey,
259 underlying_mint: &solana_pubkey::Pubkey,
260 quote_mint: &solana_pubkey::Pubkey,
261 expiry_ts: u64,
262 is_put: bool,
263) -> solana_pubkey::Pubkey {
264 solana_pubkey::Pubkey::find_program_address(
265 &[
266 b"market",
267 underlying_mint.as_ref(),
268 quote_mint.as_ref(),
269 &expiry_ts.to_le_bytes(),
270 &[u8::from(is_put)],
271 ],
272 program_id,
273 )
274 .0
275}
276
277#[derive(Debug, Clone, Copy)]
278struct Resolved {
279 price: Price,
280 valid_until: QuoteExpiry,
281 nonce: Nonce,
282 maker: [u8; 32],
283}
284
285#[derive(Debug, Clone)]
287#[must_use = "a builder does nothing until it is signed"]
288pub struct QuoteBuilder<'a> {
289 binding: &'a RfqBinding,
290 terms: OptionTerms,
291 price: Option<Price>,
292 valid_until: Option<QuoteExpiry>,
293 min_valid_until: Option<QuoteExpiry>,
294 buffer_end: Option<QuoteExpiry>,
295 nonce: Option<Nonce>,
296 maker: Option<[u8; 32]>,
297}
298
299impl QuoteBuilder<'_> {
300 #[must_use]
302 pub const fn selected_terms(&self) -> OptionTerms {
303 self.terms
304 }
305
306 pub const fn terms(mut self, terms: OptionTerms) -> Self {
308 self.terms = terms;
309 self
310 }
311
312 pub const fn price(mut self, price: Price) -> Self {
313 self.price = Some(price);
314 self
315 }
316
317 pub const fn valid_until(mut self, valid_until: QuoteExpiry) -> Self {
318 self.valid_until = Some(valid_until);
319 self
320 }
321
322 pub const fn min_valid_until(mut self, min_valid_until: QuoteExpiry) -> Self {
325 self.min_valid_until = Some(min_valid_until);
326 self
327 }
328
329 #[cfg(feature = "http")]
333 pub fn venue(self, venue: &crate::http::Venue) -> Self {
334 self.quote_window(
335 venue.min_quote_validity_seconds,
336 venue.settlement_buffer_seconds,
337 )
338 }
339
340 pub fn welcome(self, welcome: &crate::ws::types::WelcomeData) -> Self {
342 self.quote_window(
343 welcome.min_quote_validity_seconds,
344 welcome.settlement_buffer_seconds,
345 )
346 }
347
348 fn quote_window(
349 mut self,
350 min_quote_validity_seconds: u32,
351 settlement_buffer_seconds: u32,
352 ) -> Self {
353 let after = |seconds: u32| QuoteExpiry::after(Duration::from_secs(u64::from(seconds)));
354 self.min_valid_until = after(min_quote_validity_seconds);
355 self.buffer_end = after(settlement_buffer_seconds);
356 self
357 }
358
359 pub const fn nonce(mut self, nonce: Nonce) -> Self {
360 self.nonce = Some(nonce);
361 self
362 }
363
364 pub const fn maker_owner(mut self, owner: crate::Address32) -> Self {
366 self.maker = Some(*owner.as_bytes());
367 self
368 }
369
370 fn resolve(&self) -> Result<Resolved, QuoteBuildError> {
371 if !self.binding.offered_terms.contains(&self.terms) {
372 return Err(QuoteBuildError::TermsNotOffered {
373 requested: self.terms,
374 offered: self.binding.offered_terms.clone(),
375 });
376 }
377 let valid_until = self
378 .valid_until
379 .ok_or(QuoteBuildError::Missing("valid_until"))?;
380 let min_valid_until = self
381 .min_valid_until
382 .ok_or(QuoteBuildError::Missing("min_valid_until"))?;
383 if valid_until > self.binding.market_expiry {
384 return Err(QuoteBuildError::ValidUntilAfterMarketExpiry {
385 valid_until,
386 market_expiry: self.binding.market_expiry,
387 });
388 }
389 if valid_until < min_valid_until {
390 return Err(QuoteBuildError::ValidUntilTooSoon {
391 valid_until,
392 min_valid_until,
393 });
394 }
395 if let Some(buffer_end) = self.buffer_end
396 && valid_until <= buffer_end
397 {
398 return Err(QuoteBuildError::ValidUntilWithinSettlementBuffer {
399 valid_until,
400 buffer_end,
401 });
402 }
403 Ok(Resolved {
404 price: self.price.ok_or(QuoteBuildError::Missing("price"))?,
405 valid_until,
406 nonce: self.nonce.ok_or(QuoteBuildError::Missing("nonce"))?,
407 maker: self.maker.ok_or(QuoteBuildError::Missing("maker_owner"))?,
408 })
409 }
410
411 fn args_for(&self, resolved: Resolved) -> OrderPreimageArgs {
412 OrderPreimageArgs {
413 chain_id: self.binding.chain_id,
414 program_id: self.binding.program_id,
415 side: self.binding.side,
416 position_type: self.terms.position_type(),
417 market: self.binding.market,
418 strike: self.terms.strike().value(),
419 quantity: self.binding.quantity.value(),
420 gross_price: resolved.price.value(),
421 valid_until: resolved.valid_until.as_unix_seconds(),
422 maker: resolved.maker,
423 taker: self.binding.taker,
424 nonce: resolved.nonce.value(),
425 }
426 }
427
428 pub fn preimage_args(&self) -> Result<OrderPreimageArgs, QuoteBuildError> {
433 if self.terms.is_spread() {
434 return Err(crate::orders::OrderError::SpreadLayoutRequired.into());
435 }
436 Ok(self.args_for(self.resolve()?))
437 }
438
439 pub fn sign<S: SignerLike + ?Sized>(
446 &self,
447 signer: &S,
448 ) -> Result<QuoteMessage, QuoteBuildError> {
449 let (resolved, order_id, signature) = self.sign_parts(signer)?;
450 Ok(self.assemble(resolved, order_id, signature))
451 }
452
453 pub async fn sign_with_async_signer<S: AsyncSignerLike + ?Sized>(
461 &self,
462 signer: &S,
463 ) -> Result<QuoteMessage, QuoteBuildError> {
464 let resolved = self.resolve()?;
465 let order_id = self.order_id(resolved)?;
466 let signature = crate::orders::sign_order_id_with_async_signer(&order_id, signer).await?;
467 Ok(self.assemble(resolved, order_id, signature))
468 }
469
470 fn sign_parts<S: SignerLike + ?Sized>(
471 &self,
472 signer: &S,
473 ) -> Result<(Resolved, [u8; 32], [u8; 64]), QuoteBuildError> {
474 let resolved = self.resolve()?;
475 let order_id = self.order_id(resolved)?;
476 Ok((
477 resolved,
478 order_id,
479 sign_order_id_with_signer(&order_id, signer),
480 ))
481 }
482
483 pub fn spread_preimage_args(
484 &self,
485 ) -> Result<crate::orders::SpreadOrderPreimageArgs, QuoteBuildError> {
486 self.spread_args_for(self.resolve()?)
487 }
488
489 fn spread_args_for(
490 &self,
491 resolved: Resolved,
492 ) -> Result<crate::orders::SpreadOrderPreimageArgs, QuoteBuildError> {
493 let args = self.args_for(resolved);
494 Ok(crate::orders::SpreadOrderPreimageArgs {
495 chain_id: args.chain_id,
496 program_id: args.program_id,
497 side: args.side,
498 position_type: crate::spreads::SpreadType::try_from(u8::from(
499 self.terms.to_chain().position_type(),
500 ))?,
501 market: args.market,
502 lower_strike: self.terms.to_chain().strike().value(),
503 upper_strike: self
504 .terms
505 .upper_strike()
506 .ok_or(QuoteBuildError::MarketBinding("spread terms"))?
507 .value(),
508 quantity: args.quantity,
509 gross_price: args.gross_price,
510 valid_until: args.valid_until,
511 maker: args.maker,
512 taker: args.taker,
513 nonce: args.nonce,
514 })
515 }
516
517 fn order_id(&self, resolved: Resolved) -> Result<[u8; 32], QuoteBuildError> {
518 if self.terms.is_spread() {
519 Ok(crate::orders::compute_spread_order_id(
520 &self.spread_args_for(resolved)?,
521 )?)
522 } else {
523 Ok(compute_order_id(&self.args_for(resolved))?)
524 }
525 }
526
527 fn assemble(
528 &self,
529 resolved: Resolved,
530 order_id: [u8; 32],
531 signature: [u8; 64],
532 ) -> QuoteMessage {
533 QuoteMessage {
534 rfq_id: self.binding.rfq_id,
535 terms: self.terms,
536 side: self.binding.side,
537 price: resolved.price,
538 valid_until: resolved.valid_until,
539 nonce: resolved.nonce,
540 order_id: OrderId::new(order_id),
541 signature: encode_base58(&signature),
542 }
543 }
544}
545
546impl QuoteMessage {
547 #[must_use]
549 pub fn into_replacement(self, old_order_id: OrderId) -> ReplaceQuoteMessage {
550 ReplaceQuoteMessage {
551 old_order_id,
552 rfq_id: self.rfq_id,
553 terms: self.terms,
554 side: self.side,
555 price: self.price,
556 valid_until: self.valid_until,
557 nonce: self.nonce,
558 order_id: self.order_id,
559 signature: self.signature,
560 }
561 }
562}
563
564#[cfg(test)]
565#[path = "quote_tests.rs"]
566mod tests;