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 #[error("taker_buy_unsupported")]
66 TakerBuyUnsupported,
67}
68
69#[derive(Debug, Clone)]
73pub struct RfqBinding {
74 rfq_id: Uuid,
75 chain_id: u64,
76 program_id: [u8; 32],
77 market: [u8; 32],
78 market_expiry: QuoteExpiry,
79 taker: [u8; 32],
80 terms: OptionTerms,
81 quantity: Quantity,
82 offered_terms: Vec<OptionTerms>,
83 side: TakerSide,
84}
85
86impl RfqBinding {
87 pub fn from_broadcast(rfq: &RfqBroadcastMessage) -> Result<Self, QuoteBuildError> {
93 Self::from_broadcast_for_program(rfq, crate::ACTA_PROGRAM_ID)
94 }
95
96 pub fn from_broadcast_for_program(
101 rfq: &RfqBroadcastMessage,
102 expected_program: [u8; 32],
103 ) -> Result<Self, QuoteBuildError> {
104 let side = rfq.side;
105 if side.is_taker_buy() {
106 return Err(QuoteBuildError::TakerBuyUnsupported);
107 }
108 let terms = rfq.terms;
109 if terms.is_spread() {
110 let upper = terms
111 .upper_strike()
112 .ok_or(QuoteBuildError::MarketBinding("spread terms"))?;
113 if terms.position_type() != PositionType::CashPut && terms.strike().value() == 0 {
114 return Err(crate::spreads::SpreadError::InvalidStrikes.into());
115 }
116 crate::spreads::validate_strikes(
117 crate::spreads::SpreadType::try_from(u8::from(terms.to_chain().position_type()))?,
118 terms.to_chain().strike().value(),
119 upper.value(),
120 )?;
121 if rfq.quantity.value() == 0
122 || rfq.order_options.len() != 1
123 || rfq.order_options[0] != terms
124 {
125 return Err(QuoteBuildError::MarketBinding("spread pair"));
126 }
127 } else if rfq
128 .order_options
129 .iter()
130 .any(|option| option.is_spread() || option.position_type() != terms.position_type())
131 {
132 return Err(QuoteBuildError::MarketBinding("physical order_options"));
133 }
134 let (market, market_expiry) = validate_market_binding(rfq, expected_program)?;
135 let mut offered_terms = Vec::with_capacity(1 + rfq.order_options.len());
136 offered_terms.push(terms);
137 offered_terms.extend(
138 rfq.order_options
139 .iter()
140 .copied()
141 .filter(|option| *option != terms),
142 );
143
144 Ok(Self {
145 rfq_id: rfq.rfq_id,
146 chain_id: rfq.market.chain_id.value(),
147 program_id: expected_program,
148 market,
149 market_expiry,
150 taker: rfq.taker.0,
151 terms,
152 quantity: rfq.quantity,
153 offered_terms,
154 side,
155 })
156 }
157
158 #[must_use]
159 pub const fn rfq_id(&self) -> Uuid {
160 self.rfq_id
161 }
162
163 #[must_use]
165 pub const fn quantity(&self) -> Quantity {
166 self.quantity
167 }
168
169 #[must_use]
170 pub const fn position_type(&self) -> PositionType {
171 self.terms.position_type()
172 }
173
174 #[must_use]
176 pub const fn terms(&self) -> OptionTerms {
177 self.terms
178 }
179
180 #[must_use]
181 pub const fn side(&self) -> TakerSide {
182 self.side
183 }
184
185 #[must_use]
187 pub const fn market_expiry(&self) -> QuoteExpiry {
188 self.market_expiry
189 }
190
191 #[must_use]
193 pub fn offered_terms(&self) -> &[OptionTerms] {
194 &self.offered_terms
195 }
196
197 pub const fn quote(&self) -> QuoteBuilder<'_> {
199 QuoteBuilder {
200 binding: self,
201 terms: self.terms,
202 price: None,
203 valid_until: None,
204 min_valid_until: None,
205 buffer_end: None,
206 nonce: None,
207 maker: None,
208 }
209 }
210}
211
212fn validate_market_binding(
213 rfq: &RfqBroadcastMessage,
214 expected_program: [u8; 32],
215) -> Result<([u8; 32], QuoteExpiry), QuoteBuildError> {
216 use QuoteBuildError::MarketBinding;
217 use solana_pubkey::Pubkey;
218
219 let descriptor = &rfq.market;
220 if descriptor.chain_id.value() != 0 {
221 return Err(MarketBinding("chain_id"));
222 }
223 if descriptor.program_id.0 != expected_program {
224 return Err(MarketBinding("program_id"));
225 }
226 if descriptor.is_put != rfq.terms.position_type().is_put() {
227 return Err(MarketBinding("terms"));
228 }
229 let underlying = descriptor.underlying_mint.0;
230 let quote = descriptor.quote_mint.0;
231 let (collateral, settlement) = if rfq.terms.is_spread() {
232 (quote, quote)
233 } else if descriptor.is_put {
234 (quote, underlying)
235 } else {
236 (underlying, quote)
237 };
238 if descriptor.collateral_mint.0 != collateral || descriptor.settlement_mint.0 != settlement {
239 return Err(MarketBinding("collateral/settlement mint"));
240 }
241 let expiry = descriptor
242 .expiry_ts
243 .duration_since(std::time::UNIX_EPOCH)
244 .map_err(|_| MarketBinding("expiry_ts"))?;
245 if expiry.subsec_nanos() != 0 || expiry.as_secs() > i64::MAX as u64 {
246 return Err(MarketBinding("expiry_ts"));
247 }
248 let expected_market = market_pda(
249 &Pubkey::new_from_array(expected_program),
250 &Pubkey::new_from_array(underlying),
251 &Pubkey::new_from_array(quote),
252 expiry.as_secs(),
253 descriptor.is_put,
254 )
255 .to_bytes();
256 let market = descriptor.market_pda.0;
257 if market != expected_market {
258 return Err(MarketBinding("market_pda"));
259 }
260 Ok((market, QuoteExpiry::from_unix_seconds(expiry.as_secs())))
261}
262
263pub(crate) fn market_pda(
264 program_id: &solana_pubkey::Pubkey,
265 underlying_mint: &solana_pubkey::Pubkey,
266 quote_mint: &solana_pubkey::Pubkey,
267 expiry_ts: u64,
268 is_put: bool,
269) -> solana_pubkey::Pubkey {
270 solana_pubkey::Pubkey::find_program_address(
271 &[
272 b"market",
273 underlying_mint.as_ref(),
274 quote_mint.as_ref(),
275 &expiry_ts.to_le_bytes(),
276 &[u8::from(is_put)],
277 ],
278 program_id,
279 )
280 .0
281}
282
283#[derive(Debug, Clone, Copy)]
284struct Resolved {
285 price: Price,
286 valid_until: QuoteExpiry,
287 nonce: Nonce,
288 maker: [u8; 32],
289}
290
291#[derive(Debug, Clone)]
293#[must_use = "a builder does nothing until it is signed"]
294pub struct QuoteBuilder<'a> {
295 binding: &'a RfqBinding,
296 terms: OptionTerms,
297 price: Option<Price>,
298 valid_until: Option<QuoteExpiry>,
299 min_valid_until: Option<QuoteExpiry>,
300 buffer_end: Option<QuoteExpiry>,
301 nonce: Option<Nonce>,
302 maker: Option<[u8; 32]>,
303}
304
305impl QuoteBuilder<'_> {
306 #[must_use]
308 pub const fn selected_terms(&self) -> OptionTerms {
309 self.terms
310 }
311
312 pub const fn terms(mut self, terms: OptionTerms) -> Self {
314 self.terms = terms;
315 self
316 }
317
318 pub const fn price(mut self, price: Price) -> Self {
319 self.price = Some(price);
320 self
321 }
322
323 pub const fn valid_until(mut self, valid_until: QuoteExpiry) -> Self {
324 self.valid_until = Some(valid_until);
325 self
326 }
327
328 pub const fn min_valid_until(mut self, min_valid_until: QuoteExpiry) -> Self {
331 self.min_valid_until = Some(min_valid_until);
332 self
333 }
334
335 #[cfg(feature = "http")]
339 pub fn venue(self, venue: &crate::http::Venue) -> Self {
340 self.quote_window(
341 venue.min_quote_validity_seconds,
342 venue.settlement_buffer_seconds,
343 )
344 }
345
346 pub fn welcome(self, welcome: &crate::ws::types::WelcomeData) -> Self {
348 self.quote_window(
349 welcome.min_quote_validity_seconds,
350 welcome.settlement_buffer_seconds,
351 )
352 }
353
354 fn quote_window(
355 mut self,
356 min_quote_validity_seconds: u32,
357 settlement_buffer_seconds: u32,
358 ) -> Self {
359 let after = |seconds: u32| QuoteExpiry::after(Duration::from_secs(u64::from(seconds)));
360 self.min_valid_until = after(min_quote_validity_seconds);
361 self.buffer_end = after(settlement_buffer_seconds);
362 self
363 }
364
365 pub const fn nonce(mut self, nonce: Nonce) -> Self {
366 self.nonce = Some(nonce);
367 self
368 }
369
370 pub const fn maker_owner(mut self, owner: crate::Address32) -> Self {
372 self.maker = Some(*owner.as_bytes());
373 self
374 }
375
376 fn resolve(&self) -> Result<Resolved, QuoteBuildError> {
377 if !self.binding.offered_terms.contains(&self.terms) {
378 return Err(QuoteBuildError::TermsNotOffered {
379 requested: self.terms,
380 offered: self.binding.offered_terms.clone(),
381 });
382 }
383 let valid_until = self
384 .valid_until
385 .ok_or(QuoteBuildError::Missing("valid_until"))?;
386 let min_valid_until = self
387 .min_valid_until
388 .ok_or(QuoteBuildError::Missing("min_valid_until"))?;
389 if valid_until > self.binding.market_expiry {
390 return Err(QuoteBuildError::ValidUntilAfterMarketExpiry {
391 valid_until,
392 market_expiry: self.binding.market_expiry,
393 });
394 }
395 if valid_until < min_valid_until {
396 return Err(QuoteBuildError::ValidUntilTooSoon {
397 valid_until,
398 min_valid_until,
399 });
400 }
401 if let Some(buffer_end) = self.buffer_end
402 && valid_until <= buffer_end
403 {
404 return Err(QuoteBuildError::ValidUntilWithinSettlementBuffer {
405 valid_until,
406 buffer_end,
407 });
408 }
409 Ok(Resolved {
410 price: self.price.ok_or(QuoteBuildError::Missing("price"))?,
411 valid_until,
412 nonce: self.nonce.ok_or(QuoteBuildError::Missing("nonce"))?,
413 maker: self.maker.ok_or(QuoteBuildError::Missing("maker_owner"))?,
414 })
415 }
416
417 fn args_for(&self, resolved: Resolved) -> OrderPreimageArgs {
418 OrderPreimageArgs {
419 chain_id: self.binding.chain_id,
420 program_id: self.binding.program_id,
421 side: self.binding.side,
422 position_type: self.terms.position_type(),
423 market: self.binding.market,
424 strike: self.terms.strike().value(),
425 quantity: self.binding.quantity.value(),
426 gross_price: resolved.price.value(),
427 valid_until: resolved.valid_until.as_unix_seconds(),
428 maker: resolved.maker,
429 taker: self.binding.taker,
430 nonce: resolved.nonce.value(),
431 }
432 }
433
434 pub fn preimage_args(&self) -> Result<OrderPreimageArgs, QuoteBuildError> {
439 if self.terms.is_spread() {
440 return Err(crate::orders::OrderError::SpreadLayoutRequired.into());
441 }
442 Ok(self.args_for(self.resolve()?))
443 }
444
445 pub fn sign<S: SignerLike + ?Sized>(
452 &self,
453 signer: &S,
454 ) -> Result<QuoteMessage, QuoteBuildError> {
455 let (resolved, order_id, signature) = self.sign_parts(signer)?;
456 Ok(self.assemble(resolved, order_id, signature))
457 }
458
459 pub async fn sign_with_async_signer<S: AsyncSignerLike + ?Sized>(
467 &self,
468 signer: &S,
469 ) -> Result<QuoteMessage, QuoteBuildError> {
470 let resolved = self.resolve()?;
471 let order_id = self.order_id(resolved)?;
472 let signature = crate::orders::sign_order_id_with_async_signer(&order_id, signer).await?;
473 Ok(self.assemble(resolved, order_id, signature))
474 }
475
476 fn sign_parts<S: SignerLike + ?Sized>(
477 &self,
478 signer: &S,
479 ) -> Result<(Resolved, [u8; 32], [u8; 64]), QuoteBuildError> {
480 let resolved = self.resolve()?;
481 let order_id = self.order_id(resolved)?;
482 Ok((
483 resolved,
484 order_id,
485 sign_order_id_with_signer(&order_id, signer),
486 ))
487 }
488
489 pub fn spread_preimage_args(
490 &self,
491 ) -> Result<crate::orders::SpreadOrderPreimageArgs, QuoteBuildError> {
492 self.spread_args_for(self.resolve()?)
493 }
494
495 fn spread_args_for(
496 &self,
497 resolved: Resolved,
498 ) -> Result<crate::orders::SpreadOrderPreimageArgs, QuoteBuildError> {
499 let args = self.args_for(resolved);
500 Ok(crate::orders::SpreadOrderPreimageArgs {
501 chain_id: args.chain_id,
502 program_id: args.program_id,
503 side: args.side,
504 position_type: crate::spreads::SpreadType::try_from(u8::from(
505 self.terms.to_chain().position_type(),
506 ))?,
507 market: args.market,
508 lower_strike: self.terms.to_chain().strike().value(),
509 upper_strike: self
510 .terms
511 .upper_strike()
512 .ok_or(QuoteBuildError::MarketBinding("spread terms"))?
513 .value(),
514 quantity: args.quantity,
515 gross_price: args.gross_price,
516 valid_until: args.valid_until,
517 maker: args.maker,
518 taker: args.taker,
519 nonce: args.nonce,
520 })
521 }
522
523 fn order_id(&self, resolved: Resolved) -> Result<[u8; 32], QuoteBuildError> {
524 if self.terms.is_spread() {
525 Ok(crate::orders::compute_spread_order_id(
526 &self.spread_args_for(resolved)?,
527 )?)
528 } else {
529 Ok(compute_order_id(&self.args_for(resolved))?)
530 }
531 }
532
533 fn assemble(
534 &self,
535 resolved: Resolved,
536 order_id: [u8; 32],
537 signature: [u8; 64],
538 ) -> QuoteMessage {
539 QuoteMessage {
540 rfq_id: self.binding.rfq_id,
541 terms: self.terms,
542 side: self.binding.side,
543 price: resolved.price,
544 valid_until: resolved.valid_until,
545 nonce: resolved.nonce,
546 order_id: OrderId::new(order_id),
547 signature: encode_base58(&signature),
548 }
549 }
550}
551
552impl QuoteMessage {
553 #[must_use]
555 pub fn into_replacement(self, old_order_id: OrderId) -> ReplaceQuoteMessage {
556 ReplaceQuoteMessage {
557 old_order_id,
558 rfq_id: self.rfq_id,
559 terms: self.terms,
560 side: self.side,
561 price: self.price,
562 valid_until: self.valid_until,
563 nonce: self.nonce,
564 order_id: self.order_id,
565 signature: self.signature,
566 }
567 }
568}
569
570#[cfg(test)]
571#[path = "quote_tests.rs"]
572mod tests;