use crate::orders::{OrderPreimageArgs, SignerLike, compute_order_id, sign_order_id_with_signer};
use crate::signing::{AsyncSignerLike, SigningError};
use crate::types::ids::{Nonce, OrderId, Price, Quantity, Strike};
use crate::types::{PositionType, QuoteExpiry};
use crate::wire::{WireError, decode_base58_32, encode_base58};
use crate::ws::types::{QuoteMessage, ReplaceQuoteMessage, RfqBroadcastMessage};
use uuid::Uuid;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum QuoteBuildError {
#[error("rfq field `{field}` is not a valid base58 32-byte key: {source}")]
Identity {
field: &'static str,
#[source]
source: WireError,
},
#[error("strike {requested} is not offered by this rfq; offered: {offered:?}")]
StrikeNotOffered {
requested: Strike,
offered: Vec<Strike>,
},
#[error("quote is missing `{0}`")]
Missing(&'static str),
#[error("signing failed: {0}")]
Signing(#[from] SigningError),
}
#[derive(Debug, Clone)]
pub struct RfqBinding {
rfq_id: Uuid,
chain_id: u64,
program_id: [u8; 32],
market: [u8; 32],
taker: [u8; 32],
position_type: PositionType,
quantity: Quantity,
requested_strike: Strike,
offered_strikes: Vec<Strike>,
}
fn identity(field: &'static str, value: &str) -> Result<[u8; 32], QuoteBuildError> {
decode_base58_32(value).map_err(|source| QuoteBuildError::Identity { field, source })
}
impl RfqBinding {
pub fn from_broadcast(rfq: &RfqBroadcastMessage) -> Result<Self, QuoteBuildError> {
let mut offered_strikes = Vec::with_capacity(1 + rfq.order_options.len());
offered_strikes.push(rfq.strike);
offered_strikes.extend(
rfq.order_options
.iter()
.map(|option| option.strike)
.filter(|strike| *strike != rfq.strike),
);
Ok(Self {
rfq_id: rfq.rfq_id,
chain_id: rfq.market.chain_id.value(),
program_id: identity("market.program_id", &rfq.market.program_id)?,
market: identity("market.market_pda", &rfq.market.market_pda)?,
taker: identity("taker", &rfq.taker)?,
position_type: rfq.position_type,
quantity: rfq.quantity,
requested_strike: rfq.strike,
offered_strikes,
})
}
#[must_use]
pub const fn rfq_id(&self) -> Uuid {
self.rfq_id
}
#[must_use]
pub const fn quantity(&self) -> Quantity {
self.quantity
}
#[must_use]
pub const fn position_type(&self) -> PositionType {
self.position_type
}
#[must_use]
pub fn offered_strikes(&self) -> &[Strike] {
&self.offered_strikes
}
pub const fn quote(&self) -> QuoteBuilder<'_> {
QuoteBuilder {
binding: self,
strike: self.requested_strike,
price: None,
valid_until: None,
nonce: None,
maker: None,
}
}
}
#[derive(Debug, Clone, Copy)]
struct Resolved {
strike: Strike,
price: Price,
valid_until: QuoteExpiry,
nonce: Nonce,
}
#[derive(Debug, Clone)]
#[must_use = "a builder does nothing until it is signed"]
pub struct QuoteBuilder<'a> {
binding: &'a RfqBinding,
strike: Strike,
price: Option<Price>,
valid_until: Option<QuoteExpiry>,
nonce: Option<Nonce>,
maker: Option<[u8; 32]>,
}
impl QuoteBuilder<'_> {
pub const fn strike(mut self, strike: Strike) -> Self {
self.strike = strike;
self
}
pub const fn price(mut self, price: Price) -> Self {
self.price = Some(price);
self
}
pub const fn valid_until(mut self, valid_until: QuoteExpiry) -> Self {
self.valid_until = Some(valid_until);
self
}
pub const fn nonce(mut self, nonce: Nonce) -> Self {
self.nonce = Some(nonce);
self
}
pub const fn maker_owner(mut self, owner: [u8; 32]) -> Self {
self.maker = Some(owner);
self
}
fn resolve(&self) -> Result<Resolved, QuoteBuildError> {
if !self.binding.offered_strikes.contains(&self.strike) {
return Err(QuoteBuildError::StrikeNotOffered {
requested: self.strike,
offered: self.binding.offered_strikes.clone(),
});
}
Ok(Resolved {
strike: self.strike,
price: self.price.ok_or(QuoteBuildError::Missing("price"))?,
valid_until: self
.valid_until
.ok_or(QuoteBuildError::Missing("valid_until"))?,
nonce: self.nonce.ok_or(QuoteBuildError::Missing("nonce"))?,
})
}
const fn maker_for(&self, signer_pubkey: [u8; 32]) -> [u8; 32] {
match self.maker {
Some(owner) => owner,
None => signer_pubkey,
}
}
fn args_for(&self, resolved: Resolved, maker: [u8; 32]) -> OrderPreimageArgs {
OrderPreimageArgs {
chain_id: self.binding.chain_id,
program_id: self.binding.program_id,
is_taker_buy: false,
position_type: self.binding.position_type,
market: self.binding.market,
strike: resolved.strike.value(),
quantity: self.binding.quantity.value(),
gross_price: resolved.price.value(),
valid_until: resolved.valid_until.as_unix_seconds(),
maker,
taker: self.binding.taker,
nonce: resolved.nonce.value(),
}
}
pub fn preimage_args(&self, maker: [u8; 32]) -> Result<OrderPreimageArgs, QuoteBuildError> {
let resolved = self.resolve()?;
Ok(self.args_for(resolved, self.maker_for(maker)))
}
pub fn sign<S: SignerLike>(&self, signer: &S) -> Result<QuoteMessage, QuoteBuildError> {
let (resolved, order_id, signature) = self.sign_parts(signer)?;
Ok(self.assemble(resolved, order_id, signature))
}
pub async fn sign_with_async_signer<S: AsyncSignerLike + ?Sized>(
&self,
signer: &S,
) -> Result<QuoteMessage, QuoteBuildError> {
let resolved = self.resolve()?;
let args = self.args_for(resolved, self.maker_for(signer.pubkey_bytes()));
let order_id = compute_order_id(&args);
let signature = crate::orders::sign_order_id_with_async_signer(&order_id, signer).await?;
Ok(self.assemble(resolved, order_id, signature))
}
fn sign_parts<S: SignerLike>(
&self,
signer: &S,
) -> Result<(Resolved, [u8; 32], [u8; 64]), QuoteBuildError> {
let resolved = self.resolve()?;
let args = self.args_for(resolved, self.maker_for(signer.pubkey_bytes()));
let order_id = compute_order_id(&args);
Ok((
resolved,
order_id,
sign_order_id_with_signer(&order_id, signer),
))
}
fn assemble(
&self,
resolved: Resolved,
order_id: [u8; 32],
signature: [u8; 64],
) -> QuoteMessage {
QuoteMessage {
rfq_id: self.binding.rfq_id,
strike: resolved.strike,
price: resolved.price,
valid_until: resolved.valid_until,
nonce: resolved.nonce,
order_id: OrderId::new(order_id),
signature: encode_base58(&signature),
}
}
}
impl QuoteMessage {
#[must_use]
pub fn into_replacement(self, old_order_id: OrderId) -> ReplaceQuoteMessage {
ReplaceQuoteMessage {
old_order_id,
rfq_id: self.rfq_id,
strike: self.strike,
price: self.price,
valid_until: self.valid_until,
nonce: self.nonce,
order_id: self.order_id,
signature: self.signature,
}
}
}
#[cfg(test)]
#[path = "quote_tests.rs"]
mod tests;