use alloy_primitives::{Address, Bytes, U256};
use serde::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, serde_as};
use crate::{
app_data::AppDataHash,
error::{Error, Result},
order::{BuyTokenDestination, OrderClass, OrderData, OrderKind, OrderUid, SellTokenSource},
signature::Signature,
signing_scheme::SigningScheme,
};
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum OrderStatus {
PresignaturePending,
#[default]
Open,
Fulfilled,
Cancelled,
Expired,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Order {
#[serde(flatten)]
pub data: OrderData,
pub uid: OrderUid,
pub owner: Address,
pub signing_scheme: SigningScheme,
pub signature: String,
pub creation_date: String,
pub status: OrderStatus,
pub class: OrderClass,
#[serde_as(as = "DisplayFromStr")]
pub executed_buy_amount: U256,
#[serde_as(as = "DisplayFromStr")]
pub executed_sell_amount: U256,
#[serde_as(as = "Option<DisplayFromStr>")]
#[serde(default)]
pub executed_fee: Option<U256>,
#[serde(default)]
pub executed_fee_token: Option<Address>,
#[serde(default)]
pub invalidated: bool,
#[serde(default)]
pub is_liquidity_order: bool,
#[serde(default)]
pub full_app_data: Option<String>,
#[serde(default)]
pub quote: Option<serde_json::Value>,
#[serde(default)]
pub interactions: Option<serde_json::Value>,
#[serde(default)]
pub ethflow_data: Option<serde_json::Value>,
#[serde(default)]
pub onchain_order_data: Option<serde_json::Value>,
#[serde(default)]
pub onchain_user: Option<Address>,
#[serde(default)]
pub settlement_contract: Option<Address>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", try_from = "OrderCreationWire")]
pub struct OrderCreation {
pub sell_token: Address,
pub buy_token: Address,
#[serde(skip_serializing_if = "Option::is_none")]
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: String,
pub app_data_hash: AppDataHash,
#[serde_as(as = "DisplayFromStr")]
pub fee_amount: U256,
pub kind: OrderKind,
pub partially_fillable: bool,
pub sell_token_balance: SellTokenSource,
pub buy_token_balance: BuyTokenDestination,
pub signing_scheme: SigningScheme,
#[serde(serialize_with = "serialise_signature_bytes")]
pub signature: Signature,
pub from: Address,
#[serde(skip_serializing_if = "Option::is_none")]
pub quote_id: Option<i64>,
}
fn serialise_signature_bytes<S>(
signature: &Signature,
serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
Bytes::from(signature.to_bytes()).serialize(serializer)
}
#[serde_as]
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct OrderCreationWire {
sell_token: Address,
buy_token: Address,
#[serde(default)]
receiver: Option<Address>,
#[serde_as(as = "DisplayFromStr")]
sell_amount: U256,
#[serde_as(as = "DisplayFromStr")]
buy_amount: U256,
valid_to: u32,
app_data: String,
app_data_hash: AppDataHash,
#[serde_as(as = "DisplayFromStr")]
fee_amount: U256,
kind: OrderKind,
partially_fillable: bool,
sell_token_balance: SellTokenSource,
buy_token_balance: BuyTokenDestination,
signing_scheme: SigningScheme,
signature: Bytes,
from: Address,
#[serde(default)]
quote_id: Option<i64>,
}
impl TryFrom<OrderCreationWire> for OrderCreation {
type Error = crate::error::Error;
fn try_from(wire: OrderCreationWire) -> std::result::Result<Self, Self::Error> {
let signature = Signature::from_bytes(wire.signing_scheme, &wire.signature)?;
let order_data = OrderData {
sell_token: wire.sell_token,
buy_token: wire.buy_token,
receiver: wire.receiver,
sell_amount: wire.sell_amount,
buy_amount: wire.buy_amount,
valid_to: wire.valid_to,
app_data: wire.app_data_hash,
fee_amount: wire.fee_amount,
kind: wire.kind,
partially_fillable: wire.partially_fillable,
sell_token_balance: wire.sell_token_balance,
buy_token_balance: wire.buy_token_balance,
};
Self::from_signed_order_data(
&order_data,
signature,
wire.from,
wire.app_data,
wire.quote_id,
)
}
}
impl OrderCreation {
pub const fn order_data(&self) -> OrderData {
OrderData {
sell_token: self.sell_token,
buy_token: self.buy_token,
receiver: self.receiver,
sell_amount: self.sell_amount,
buy_amount: self.buy_amount,
valid_to: self.valid_to,
app_data: self.app_data_hash,
fee_amount: self.fee_amount,
kind: self.kind,
partially_fillable: self.partially_fillable,
sell_token_balance: self.sell_token_balance,
buy_token_balance: self.buy_token_balance,
}
}
pub fn verify_owner(
&self,
domain: &crate::domain::DomainSeparator,
) -> std::result::Result<Address, crate::signature::SignatureError> {
let payload = crate::order::eip712::Order::from(&self.order_data());
match self.signature.recover(domain, &payload)? {
Some(recovered) if recovered.signer == self.from => Ok(self.from),
Some(recovered) => Err(crate::signature::SignatureError::SignerMismatch {
declared: self.from,
recovered: recovered.signer,
}),
None if self.from == Address::ZERO => {
Err(crate::signature::SignatureError::SignerMismatch {
declared: Address::ZERO,
recovered: Address::ZERO,
})
}
None => Ok(self.from),
}
}
pub fn from_signed_order_data(
order_data: &OrderData,
signature: Signature,
from: Address,
app_data_json: String,
quote_id: Option<i64>,
) -> Result<Self> {
if from == Address::ZERO {
return Err(Error::OrderCreationInvalid {
field: "from",
reason: "owner address must be non-zero",
});
}
let json_digest = alloy_primitives::keccak256(app_data_json.as_bytes());
if json_digest != order_data.app_data {
return Err(Error::OrderCreationInvalid {
field: "app_data",
reason: "JSON digest does not match signed app_data hash",
});
}
let receiver = match order_data.receiver {
Some(addr) if addr == Address::ZERO => None,
other => other,
};
Ok(Self {
sell_token: order_data.sell_token,
buy_token: order_data.buy_token,
receiver,
sell_amount: order_data.sell_amount,
buy_amount: order_data.buy_amount,
valid_to: order_data.valid_to,
app_data: app_data_json,
app_data_hash: order_data.app_data,
fee_amount: order_data.fee_amount,
kind: order_data.kind,
partially_fillable: order_data.partially_fillable,
sell_token_balance: order_data.sell_token_balance,
buy_token_balance: order_data.buy_token_balance,
signing_scheme: signature.scheme(),
signature,
from,
quote_id,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app_data::{EMPTY_APP_DATA_HASH, EMPTY_APP_DATA_JSON};
use crate::domain::{DomainSeparator, settlement_domain};
use crate::signing_scheme::EcdsaSigningScheme;
use alloy_primitives::address;
use alloy_signer_local::PrivateKeySigner;
const SETTLEMENT: Address = address!("9008D19f58AAbD9eD0D60971565AA8510560ab41");
fn zero_eip712_signature() -> Signature {
Signature::Eip712(crate::signature::EcdsaSignature::from_bytes_and_parity(
&[0u8; 64], false,
))
}
fn empty_app_data_order() -> OrderData {
OrderData {
sell_token: address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
buy_token: address!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
receiver: None,
sell_amount: U256::from(1_000_000u64),
buy_amount: U256::from(999u64),
valid_to: 0xffff_ffff,
app_data: EMPTY_APP_DATA_HASH,
fee_amount: U256::ZERO,
kind: OrderKind::Sell,
partially_fillable: false,
sell_token_balance: SellTokenSource::default(),
buy_token_balance: BuyTokenDestination::default(),
}
}
fn signer() -> PrivateKeySigner {
PrivateKeySigner::from_bytes(&U256::from(1u64).to_be_bytes().into()).unwrap()
}
#[test]
fn from_signed_order_data_rejects_zero_from_address() {
let err = OrderCreation::from_signed_order_data(
&OrderData::default(),
zero_eip712_signature(),
Address::ZERO,
EMPTY_APP_DATA_JSON.to_owned(),
None,
)
.unwrap_err();
assert!(
matches!(err, Error::OrderCreationInvalid { field: "from", .. }),
"got: {err}"
);
}
#[test]
fn from_signed_order_data_rejects_app_data_digest_mismatch() {
let err = OrderCreation::from_signed_order_data(
&empty_app_data_order(),
zero_eip712_signature(),
address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"),
r#"{"version":"1.6.0","metadata":{}}"#.to_owned(),
None,
)
.unwrap_err();
assert!(
matches!(
err,
Error::OrderCreationInvalid {
field: "app_data",
..
}
),
"got: {err}"
);
}
#[test]
fn deserialise_rejects_app_data_digest_mismatch() {
let creation = OrderCreation::from_signed_order_data(
&empty_app_data_order(),
zero_eip712_signature(),
address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"),
EMPTY_APP_DATA_JSON.to_owned(),
None,
)
.unwrap();
let mut body = serde_json::to_value(creation).unwrap();
body["appData"] = serde_json::Value::String(r#"{"version":"1.6.0","metadata":{}}"#.into());
let err = serde_json::from_value::<OrderCreation>(body).unwrap_err();
assert!(
err.to_string().contains("app_data"),
"expected app_data digest mismatch surfaced through serde, got: {err}"
);
}
#[test]
fn verify_owner_rejects_zero_from_for_onchain_schemes() {
let creation = OrderCreation {
sell_token: Address::ZERO,
buy_token: Address::ZERO,
receiver: None,
sell_amount: U256::ZERO,
buy_amount: U256::ZERO,
valid_to: 0,
app_data: EMPTY_APP_DATA_JSON.to_owned(),
app_data_hash: EMPTY_APP_DATA_HASH,
fee_amount: U256::ZERO,
kind: OrderKind::Sell,
partially_fillable: false,
sell_token_balance: SellTokenSource::default(),
buy_token_balance: BuyTokenDestination::default(),
signing_scheme: SigningScheme::PreSign,
signature: Signature::PreSign,
from: Address::ZERO,
quote_id: None,
};
let err = creation
.verify_owner(&DomainSeparator::default())
.unwrap_err();
assert!(matches!(
err,
crate::signature::SignatureError::SignerMismatch { .. }
));
}
#[test]
fn verify_owner_rejects_signer_mismatch_for_ecdsa() {
let signer = signer();
let real_signer = signer.address();
let impostor = address!("dead0000dead0000dead0000dead0000dead0000");
assert_ne!(real_signer, impostor);
let domain = settlement_domain(1, SETTLEMENT);
let order_data = empty_app_data_order();
let signature = order_data
.sign(EcdsaSigningScheme::Eip712, &domain, &signer)
.unwrap();
let creation = OrderCreation::from_signed_order_data(
&order_data,
signature,
impostor,
EMPTY_APP_DATA_JSON.to_owned(),
None,
)
.unwrap();
let err = creation.verify_owner(&domain).unwrap_err();
match err {
crate::signature::SignatureError::SignerMismatch {
declared,
recovered,
} => {
assert_eq!(declared, impostor);
assert_eq!(recovered, real_signer);
}
other => panic!("expected SignerMismatch, got {other:?}"),
}
}
#[test]
fn verify_owner_succeeds_for_matching_ecdsa_signer() {
let signer = signer();
let owner = signer.address();
let domain = settlement_domain(1, SETTLEMENT);
let order_data = empty_app_data_order();
let signature = order_data
.sign(EcdsaSigningScheme::Eip712, &domain, &signer)
.unwrap();
let creation = OrderCreation::from_signed_order_data(
&order_data,
signature,
owner,
EMPTY_APP_DATA_JSON.to_owned(),
None,
)
.unwrap();
assert_eq!(creation.verify_owner(&domain).unwrap(), owner);
}
}