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