use alloy_primitives::{Address, U256, keccak256};
use serde::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, serde_as};
use crate::app_data::AppDataHash;
use crate::error::{Error, Result};
use crate::order::{BuyTokenDestination, OrderData, OrderKind, SellTokenSource};
use crate::quote_amounts::{OrderCosts, ProtocolFeeBps, QuoteAmountsAndCosts, QuoteAmountsParams};
use crate::signing_scheme::SigningScheme;
use super::types::{PriceQuality, QuoteAppData};
#[serde_as]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QuoteRequest {
pub sell_token: Address,
pub buy_token: Address,
pub from: Address,
#[serde(skip_serializing_if = "Option::is_none")]
pub receiver: Option<Address>,
pub(crate) kind: OrderKind,
#[serde_as(as = "Option<DisplayFromStr>")]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) sell_amount_before_fee: Option<U256>,
#[serde_as(as = "Option<DisplayFromStr>")]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) sell_amount_after_fee: Option<U256>,
#[serde_as(as = "Option<DisplayFromStr>")]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) buy_amount_after_fee: Option<U256>,
#[serde(skip_serializing_if = "Option::is_none")]
pub valid_to: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub valid_for: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub app_data: Option<QuoteAppData>,
#[serde(skip_serializing_if = "Option::is_none")]
pub partially_fillable: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sell_token_balance: Option<SellTokenSource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub buy_token_balance: Option<BuyTokenDestination>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signing_scheme: Option<SigningScheme>,
#[serde(skip_serializing_if = "Option::is_none")]
pub verification_gas_limit: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub onchain_order: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub price_quality: Option<PriceQuality>,
}
impl QuoteRequest {
pub const fn sell_before_fee(
sell_token: Address,
buy_token: Address,
from: Address,
sell_amount: U256,
) -> Self {
Self::new(sell_token, buy_token, from, OrderKind::Sell)
.with_sell_amount_before_fee(sell_amount)
}
pub const fn sell_after_fee(
sell_token: Address,
buy_token: Address,
from: Address,
sell_amount: U256,
) -> Self {
Self::new(sell_token, buy_token, from, OrderKind::Sell)
.with_sell_amount_after_fee(sell_amount)
}
pub const fn buy_after_fee(
sell_token: Address,
buy_token: Address,
from: Address,
buy_amount: U256,
) -> Self {
Self::new(sell_token, buy_token, from, OrderKind::Buy).with_buy_amount_after_fee(buy_amount)
}
const fn new(sell_token: Address, buy_token: Address, from: Address, kind: OrderKind) -> Self {
Self {
sell_token,
buy_token,
from,
receiver: None,
kind,
sell_amount_before_fee: None,
sell_amount_after_fee: None,
buy_amount_after_fee: None,
valid_to: None,
valid_for: None,
app_data: None,
partially_fillable: None,
sell_token_balance: None,
buy_token_balance: None,
signing_scheme: None,
verification_gas_limit: None,
onchain_order: None,
price_quality: None,
}
}
const fn with_sell_amount_before_fee(mut self, a: U256) -> Self {
self.sell_amount_before_fee = Some(a);
self
}
const fn with_sell_amount_after_fee(mut self, a: U256) -> Self {
self.sell_amount_after_fee = Some(a);
self
}
const fn with_buy_amount_after_fee(mut self, a: U256) -> Self {
self.buy_amount_after_fee = Some(a);
self
}
pub const fn kind(&self) -> OrderKind {
self.kind
}
pub fn validate(&self) -> Result<()> {
if self.valid_to.is_some() && self.valid_for.is_some() {
return Err(Error::QuoteRequestInvalid {
field: "validTo/validFor",
reason: "are mutually exclusive; set at most one",
});
}
let count = u8::from(self.sell_amount_before_fee.is_some())
+ u8::from(self.sell_amount_after_fee.is_some())
+ u8::from(self.buy_amount_after_fee.is_some());
if count != 1 {
return Err(Error::QuoteRequestInvalid {
field: "amount",
reason: "exactly one of sellAmountBeforeFee, sellAmountAfterFee, \
buyAmountAfterFee must be set",
});
}
let kind_for_amount = if self.buy_amount_after_fee.is_some() {
OrderKind::Buy
} else {
OrderKind::Sell
};
if self.kind != kind_for_amount {
return Err(Error::QuoteRequestInvalid {
field: "kind",
reason: "does not agree with the set amount; sell amounts \
require Sell, the buy amount requires Buy",
});
}
Ok(())
}
}
#[serde_as]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderQuote {
pub sell_token: Address,
pub buy_token: Address,
#[serde(default)]
pub receiver: Option<Address>,
#[serde_as(as = "DisplayFromStr")]
pub sell_amount: U256,
#[serde_as(as = "DisplayFromStr")]
pub buy_amount: U256,
pub valid_to: u32,
pub app_data: AppDataHash,
#[serde_as(as = "DisplayFromStr")]
pub fee_amount: U256,
pub kind: OrderKind,
pub partially_fillable: bool,
#[serde(default)]
pub sell_token_balance: SellTokenSource,
#[serde(default)]
pub buy_token_balance: BuyTokenDestination,
pub signing_scheme: SigningScheme,
}
impl OrderQuoteResponse {
pub fn try_to_order_data(
&self,
request: &QuoteRequest,
app_data: AppDataHash,
costs: &OrderCosts,
) -> Result<OrderData> {
self.check_response_matches_request(request, app_data)?;
let amounts = self.amounts_with_costs(costs)?;
Ok(self.project_into_order_data(
amounts.amounts_to_sign.sell_amount,
amounts.amounts_to_sign.buy_amount,
app_data,
))
}
const fn project_into_order_data(
&self,
sell_amount: U256,
buy_amount: U256,
app_data: AppDataHash,
) -> OrderData {
let q = &self.quote;
OrderData {
sell_token: q.sell_token,
buy_token: q.buy_token,
receiver: q.receiver,
sell_amount,
buy_amount,
valid_to: q.valid_to,
app_data,
fee_amount: U256::ZERO,
kind: q.kind,
partially_fillable: q.partially_fillable,
sell_token_balance: q.sell_token_balance,
buy_token_balance: q.buy_token_balance,
}
}
pub fn amounts_with_costs(&self, costs: &OrderCosts) -> Result<QuoteAmountsAndCosts> {
let q = &self.quote;
crate::quote_amounts::compute(QuoteAmountsParams {
kind: q.kind,
sell_amount: q.sell_amount,
buy_amount: q.buy_amount,
fee_amount: q.fee_amount,
partner_fee_bps: costs.partner_fee_bps,
slippage_bps: costs.slippage_bps,
protocol_fee_bps: costs.protocol_fee_bps_override.or(self.protocol_fee_bps),
})
}
pub(crate) fn check_response_matches_request(
&self,
request: &QuoteRequest,
app_data: AppDataHash,
) -> Result<()> {
let q = &self.quote;
ensure_eq("sellToken", request.sell_token, q.sell_token)?;
ensure_eq("buyToken", request.buy_token, q.buy_token)?;
ensure_eq("from", request.from, self.from)?;
ensure_eq("kind", request.kind, q.kind)?;
let normalise = |owner: Address, receiver: Option<Address>| match receiver {
Some(addr) if addr == Address::ZERO || addr == owner => None,
other => other,
};
ensure_eq(
"receiver",
normalise(request.from, request.receiver),
normalise(request.from, q.receiver),
)?;
if let Some(valid_to) = request.valid_to {
ensure_eq("validTo", valid_to, q.valid_to)?;
}
if let Some(partially_fillable) = request.partially_fillable {
ensure_eq(
"partiallyFillable",
partially_fillable,
q.partially_fillable,
)?;
}
if let Some(src) = request.sell_token_balance {
ensure_eq("sellTokenBalance", src, q.sell_token_balance)?;
}
if let Some(dst) = request.buy_token_balance {
ensure_eq("buyTokenBalance", dst, q.buy_token_balance)?;
}
if let Some(scheme) = request.signing_scheme {
ensure_eq("signingScheme", scheme, q.signing_scheme)?;
}
if let Some(QuoteAppData::Hash(requested_hash)) = request.app_data.as_ref() {
ensure_eq("appData", *requested_hash, app_data)?;
}
if let Some(QuoteAppData::Full(json)) = request.app_data.as_ref() {
ensure_eq("appData", keccak256(json.as_bytes()), app_data)?;
}
if let Some(requested) = request.sell_amount_before_fee {
let signed_sell =
q.sell_amount
.checked_add(q.fee_amount)
.ok_or(Error::QuoteFeeMathOverflow {
stage: "request_binding.sell_before_fee",
})?;
ensure_eq("sellAmountBeforeFee", requested, signed_sell)?;
}
if let Some(requested) = request.sell_amount_after_fee {
ensure_eq("sellAmountAfterFee", requested, q.sell_amount)?;
}
if let Some(requested) = request.buy_amount_after_fee {
ensure_eq("buyAmountAfterFee", requested, q.buy_amount)?;
}
Ok(())
}
}
pub(crate) fn ensure_eq<T>(field: &'static str, requested: T, returned: T) -> Result<()>
where
T: core::fmt::Debug + PartialEq,
{
if requested == returned {
return Ok(());
}
Err(Error::QuoteFieldMismatch {
field,
requested: format!("{requested:?}"),
returned: format!("{returned:?}"),
})
}
#[serde_as]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderQuoteResponse {
pub quote: OrderQuote,
pub from: Address,
pub expiration: String,
pub id: i64,
pub verified: bool,
#[serde_as(as = "Option<DisplayFromStr>")]
#[serde(default)]
pub protocol_fee_bps: Option<ProtocolFeeBps>,
}