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, Strike};
8use crate::types::{PositionType, QuoteExpiry};
9use crate::wire::{WireError, decode_base58_32, encode_base58};
10use crate::ws::types::{QuoteMessage, ReplaceQuoteMessage, RfqBroadcastMessage};
11use uuid::Uuid;
12
13#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum QuoteBuildError {
17 #[error("rfq field `{field}` is not a valid base58 32-byte key: {source}")]
18 Identity {
19 field: &'static str,
20 #[source]
21 source: WireError,
22 },
23
24 #[error("rfq market does not match trusted signing terms: {0}")]
25 MarketBinding(&'static str),
26
27 #[error("strike {requested} is not offered by this rfq; offered: {offered:?}")]
29 StrikeNotOffered {
30 requested: Strike,
31 offered: Vec<Strike>,
32 },
33
34 #[error("quote is missing `{0}`")]
35 Missing(&'static str),
36
37 #[error("signing failed: {0}")]
38 Signing(#[from] SigningError),
39}
40
41#[derive(Debug, Clone)]
45pub struct RfqBinding {
46 rfq_id: Uuid,
47 chain_id: u64,
48 program_id: [u8; 32],
49 market: [u8; 32],
50 taker: [u8; 32],
51 position_type: PositionType,
52 quantity: Quantity,
53 requested_strike: Strike,
54 offered_strikes: Vec<Strike>,
55}
56
57fn identity(field: &'static str, value: &str) -> Result<[u8; 32], QuoteBuildError> {
58 decode_base58_32(value).map_err(|source| QuoteBuildError::Identity { field, source })
59}
60
61impl RfqBinding {
62 pub fn from_broadcast(rfq: &RfqBroadcastMessage) -> Result<Self, QuoteBuildError> {
68 Self::from_broadcast_for_program(rfq, crate::ACTA_PROGRAM_ID)
69 }
70
71 pub fn from_broadcast_for_program(
76 rfq: &RfqBroadcastMessage,
77 expected_program: [u8; 32],
78 ) -> Result<Self, QuoteBuildError> {
79 let market = validate_market_binding(rfq, expected_program)?;
80 let mut offered_strikes = Vec::with_capacity(1 + rfq.order_options.len());
81 offered_strikes.push(rfq.strike);
82 offered_strikes.extend(
83 rfq.order_options
84 .iter()
85 .map(|option| option.strike)
86 .filter(|strike| *strike != rfq.strike),
87 );
88
89 Ok(Self {
90 rfq_id: rfq.rfq_id,
91 chain_id: rfq.market.chain_id.value(),
92 program_id: expected_program,
93 market,
94 taker: identity("taker", &rfq.taker)?,
95 position_type: rfq.position_type,
96 quantity: rfq.quantity,
97 requested_strike: rfq.strike,
98 offered_strikes,
99 })
100 }
101
102 #[must_use]
103 pub const fn rfq_id(&self) -> Uuid {
104 self.rfq_id
105 }
106
107 #[must_use]
109 pub const fn quantity(&self) -> Quantity {
110 self.quantity
111 }
112
113 #[must_use]
114 pub const fn position_type(&self) -> PositionType {
115 self.position_type
116 }
117
118 #[must_use]
120 pub fn offered_strikes(&self) -> &[Strike] {
121 &self.offered_strikes
122 }
123
124 pub const fn quote(&self) -> QuoteBuilder<'_> {
126 QuoteBuilder {
127 binding: self,
128 strike: self.requested_strike,
129 price: None,
130 valid_until: None,
131 nonce: None,
132 maker: None,
133 }
134 }
135}
136
137fn validate_market_binding(
138 rfq: &RfqBroadcastMessage,
139 expected_program: [u8; 32],
140) -> Result<[u8; 32], QuoteBuildError> {
141 use QuoteBuildError::MarketBinding;
142 use solana_pubkey::Pubkey;
143
144 let descriptor = &rfq.market;
145 if descriptor.chain_id.value() != 0 {
146 return Err(MarketBinding("chain_id"));
147 }
148 if identity("market.program_id", &descriptor.program_id)? != expected_program {
149 return Err(MarketBinding("program_id"));
150 }
151 if descriptor.is_put != (rfq.position_type == PositionType::CashSecuredPut) {
152 return Err(MarketBinding("position_type"));
153 }
154 let underlying = identity("market.underlying_mint", &descriptor.underlying_mint)?;
155 let quote = identity("market.quote_mint", &descriptor.quote_mint)?;
156 let (collateral, settlement) = if descriptor.is_put {
157 (quote, underlying)
158 } else {
159 (underlying, quote)
160 };
161 if identity("market.collateral_mint", &descriptor.collateral_mint)? != collateral
162 || identity("market.settlement_mint", &descriptor.settlement_mint)? != settlement
163 {
164 return Err(MarketBinding("collateral/settlement mint"));
165 }
166 let expiry = descriptor
167 .expiry_ts
168 .duration_since(std::time::UNIX_EPOCH)
169 .map_err(|_| MarketBinding("expiry_ts"))?;
170 if expiry.subsec_nanos() != 0 || expiry.as_secs() > i64::MAX as u64 {
171 return Err(MarketBinding("expiry_ts"));
172 }
173 let expected_market = market_pda(
174 &Pubkey::new_from_array(expected_program),
175 &Pubkey::new_from_array(underlying),
176 &Pubkey::new_from_array(quote),
177 expiry.as_secs(),
178 descriptor.is_put,
179 )
180 .to_bytes();
181 let market = identity("market.market_pda", &descriptor.market_pda)?;
182 if market != expected_market {
183 return Err(MarketBinding("market_pda"));
184 }
185 Ok(market)
186}
187
188pub(crate) fn market_pda(
189 program_id: &solana_pubkey::Pubkey,
190 underlying_mint: &solana_pubkey::Pubkey,
191 quote_mint: &solana_pubkey::Pubkey,
192 expiry_ts: u64,
193 is_put: bool,
194) -> solana_pubkey::Pubkey {
195 solana_pubkey::Pubkey::find_program_address(
196 &[
197 b"market",
198 underlying_mint.as_ref(),
199 quote_mint.as_ref(),
200 &expiry_ts.to_le_bytes(),
201 &[u8::from(is_put)],
202 ],
203 program_id,
204 )
205 .0
206}
207
208#[derive(Debug, Clone, Copy)]
209struct Resolved {
210 strike: Strike,
211 price: Price,
212 valid_until: QuoteExpiry,
213 nonce: Nonce,
214}
215
216#[derive(Debug, Clone)]
218#[must_use = "a builder does nothing until it is signed"]
219pub struct QuoteBuilder<'a> {
220 binding: &'a RfqBinding,
221 strike: Strike,
222 price: Option<Price>,
223 valid_until: Option<QuoteExpiry>,
224 nonce: Option<Nonce>,
225 maker: Option<[u8; 32]>,
226}
227
228impl QuoteBuilder<'_> {
229 pub const fn strike(mut self, strike: Strike) -> Self {
231 self.strike = strike;
232 self
233 }
234
235 pub const fn price(mut self, price: Price) -> Self {
236 self.price = Some(price);
237 self
238 }
239
240 pub const fn valid_until(mut self, valid_until: QuoteExpiry) -> Self {
241 self.valid_until = Some(valid_until);
242 self
243 }
244
245 pub const fn nonce(mut self, nonce: Nonce) -> Self {
246 self.nonce = Some(nonce);
247 self
248 }
249
250 pub const fn maker_owner(mut self, owner: [u8; 32]) -> Self {
253 self.maker = Some(owner);
254 self
255 }
256
257 fn resolve(&self) -> Result<Resolved, QuoteBuildError> {
258 if !self.binding.offered_strikes.contains(&self.strike) {
259 return Err(QuoteBuildError::StrikeNotOffered {
260 requested: self.strike,
261 offered: self.binding.offered_strikes.clone(),
262 });
263 }
264 Ok(Resolved {
265 strike: self.strike,
266 price: self.price.ok_or(QuoteBuildError::Missing("price"))?,
267 valid_until: self
268 .valid_until
269 .ok_or(QuoteBuildError::Missing("valid_until"))?,
270 nonce: self.nonce.ok_or(QuoteBuildError::Missing("nonce"))?,
271 })
272 }
273
274 const fn maker_for(&self, signer_pubkey: [u8; 32]) -> [u8; 32] {
275 match self.maker {
276 Some(owner) => owner,
277 None => signer_pubkey,
278 }
279 }
280
281 fn args_for(&self, resolved: Resolved, maker: [u8; 32]) -> OrderPreimageArgs {
282 OrderPreimageArgs {
283 chain_id: self.binding.chain_id,
284 program_id: self.binding.program_id,
285 is_taker_buy: false,
286 position_type: self.binding.position_type,
287 market: self.binding.market,
288 strike: resolved.strike.value(),
289 quantity: self.binding.quantity.value(),
290 gross_price: resolved.price.value(),
291 valid_until: resolved.valid_until.as_unix_seconds(),
292 maker,
293 taker: self.binding.taker,
294 nonce: resolved.nonce.value(),
295 }
296 }
297
298 pub fn preimage_args(&self, maker: [u8; 32]) -> Result<OrderPreimageArgs, QuoteBuildError> {
306 let resolved = self.resolve()?;
307 Ok(self.args_for(resolved, self.maker_for(maker)))
308 }
309
310 pub fn sign<S: SignerLike + ?Sized>(
317 &self,
318 signer: &S,
319 ) -> Result<QuoteMessage, QuoteBuildError> {
320 let (resolved, order_id, signature) = self.sign_parts(signer)?;
321 Ok(self.assemble(resolved, order_id, signature))
322 }
323
324 pub async fn sign_with_async_signer<S: AsyncSignerLike + ?Sized>(
332 &self,
333 signer: &S,
334 ) -> Result<QuoteMessage, QuoteBuildError> {
335 let resolved = self.resolve()?;
336 let args = self.args_for(resolved, self.maker_for(signer.pubkey_bytes()));
337 let order_id = compute_order_id(&args);
338 let signature = crate::orders::sign_order_id_with_async_signer(&order_id, signer).await?;
339 Ok(self.assemble(resolved, order_id, signature))
340 }
341
342 fn sign_parts<S: SignerLike + ?Sized>(
343 &self,
344 signer: &S,
345 ) -> Result<(Resolved, [u8; 32], [u8; 64]), QuoteBuildError> {
346 let resolved = self.resolve()?;
347 let args = self.args_for(resolved, self.maker_for(signer.pubkey_bytes()));
348 let order_id = compute_order_id(&args);
349 Ok((
350 resolved,
351 order_id,
352 sign_order_id_with_signer(&order_id, signer),
353 ))
354 }
355
356 fn assemble(
357 &self,
358 resolved: Resolved,
359 order_id: [u8; 32],
360 signature: [u8; 64],
361 ) -> QuoteMessage {
362 QuoteMessage {
363 rfq_id: self.binding.rfq_id,
364 strike: resolved.strike,
365 price: resolved.price,
366 valid_until: resolved.valid_until,
367 nonce: resolved.nonce,
368 order_id: OrderId::new(order_id),
369 signature: encode_base58(&signature),
370 }
371 }
372}
373
374impl QuoteMessage {
375 #[must_use]
377 pub fn into_replacement(self, old_order_id: OrderId) -> ReplaceQuoteMessage {
378 ReplaceQuoteMessage {
379 old_order_id,
380 rfq_id: self.rfq_id,
381 strike: self.strike,
382 price: self.price,
383 valid_until: self.valid_until,
384 nonce: self.nonce,
385 order_id: self.order_id,
386 signature: self.signature,
387 }
388 }
389}
390
391#[cfg(test)]
392#[path = "quote_tests.rs"]
393mod tests;