use alloy_primitives::{Address, Bytes, U256};
use serde::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, serde_as};
#[cfg(not(target_arch = "wasm32"))]
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::{
app_data::AppDataHash,
error::{Error, Result},
order::{BuyTokenDestination, OrderClass, OrderData, OrderKind, OrderUid, SellTokenSource},
signature::Signature,
signing_scheme::SigningScheme,
};
pub const DEFAULT_ORDER_VALID_TO_MAX_HORIZON_SECS: u64 = 31_536_000;
#[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::new(
&order_data,
signature,
wire.from,
wire.app_data,
wire.quote_id,
)
}
}
#[cfg(not(target_arch = "wasm32"))]
fn current_epoch_seconds() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs()
}
#[cfg(target_arch = "wasm32")]
fn current_epoch_seconds() -> u64 {
let millis = js_sys::Date::now();
if millis.is_finite() && millis > 0.0 {
(millis / 1_000.0) as u64
} else {
0
}
}
#[cfg(test)]
pub(in crate::order_book) fn valid_to_after_for_tests(seconds: u64) -> u32 {
current_epoch_seconds()
.saturating_add(seconds)
.min(u64::from(u32::MAX)) as u32
}
fn validate_valid_to_max_horizon(
valid_to: u32,
signing_scheme: SigningScheme,
max_horizon_secs: u64,
) -> Result<()> {
if signing_scheme == SigningScheme::PreSign {
return Ok(());
}
let max_valid_to = current_epoch_seconds()
.saturating_add(max_horizon_secs)
.min(u64::from(u32::MAX));
if u64::from(valid_to) > max_valid_to {
return Err(Error::OrderCreationInvalid {
field: "valid_to",
reason: "valid_to exceeds the configured maximum horizon",
});
}
Ok(())
}
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::error::VerifyOwnerError> {
match self.order_data().recover_signer(domain, &self.signature)? {
Some(recovered) if recovered.signer == self.from => Ok(self.from),
Some(recovered) => Err(crate::error::VerifyOwnerError::SignerMismatch {
declared: self.from,
recovered: recovered.signer,
}),
None if self.from == Address::ZERO => {
Err(crate::error::VerifyOwnerError::SignerMismatch {
declared: Address::ZERO,
recovered: Address::ZERO,
})
}
None => Ok(self.from),
}
}
pub fn new(
order_data: &OrderData,
signature: Signature,
from: Address,
app_data_json: String,
quote_id: Option<i64>,
) -> Result<Self> {
Self::new_with_valid_to_max_horizon_secs(
order_data,
signature,
from,
app_data_json,
quote_id,
DEFAULT_ORDER_VALID_TO_MAX_HORIZON_SECS,
)
}
pub fn new_with_valid_to_max_horizon_secs(
order_data: &OrderData,
signature: Signature,
from: Address,
app_data_json: String,
quote_id: Option<i64>,
max_horizon_secs: u64,
) -> Result<Self> {
if from == Address::ZERO {
return Err(Error::OrderCreationInvalid {
field: "from",
reason: "owner address must be non-zero",
});
}
let signing_scheme = signature.scheme();
validate_valid_to_max_horizon(order_data.valid_to, signing_scheme, max_horizon_secs)?;
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,
from,
quote_id,
})
}
pub const fn builder(
order_data: &OrderData,
signature: Signature,
from: Address,
app_data_json: String,
) -> OrderCreationBuilder {
OrderCreationBuilder::new(order_data, signature, from, app_data_json)
}
#[deprecated(since = "0.2.0", note = "renamed to OrderCreation::new")]
pub fn from_signed_order_data(
order_data: &OrderData,
signature: Signature,
from: Address,
app_data_json: String,
quote_id: Option<i64>,
) -> Result<Self> {
Self::new(order_data, signature, from, app_data_json, quote_id)
}
}
#[derive(Clone, Debug)]
pub struct OrderCreationBuilder {
order_data: OrderData,
signature: Signature,
from: Address,
app_data_json: String,
quote_id: Option<i64>,
valid_to_max_horizon_secs: u64,
}
impl OrderCreationBuilder {
const fn new(
order_data: &OrderData,
signature: Signature,
from: Address,
app_data_json: String,
) -> Self {
Self {
order_data: *order_data,
signature,
from,
app_data_json,
quote_id: None,
valid_to_max_horizon_secs: DEFAULT_ORDER_VALID_TO_MAX_HORIZON_SECS,
}
}
#[must_use]
pub const fn with_quote_id(mut self, quote_id: i64) -> Self {
self.quote_id = Some(quote_id);
self
}
#[must_use]
pub const fn with_valid_to_max_horizon_secs(mut self, seconds: u64) -> Self {
self.valid_to_max_horizon_secs = seconds;
self
}
pub fn build(self) -> Result<OrderCreation> {
OrderCreation::new_with_valid_to_max_horizon_secs(
&self.order_data,
self.signature,
self.from,
self.app_data_json,
self.quote_id,
self.valid_to_max_horizon_secs,
)
}
}
#[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: valid_to_after_for_tests(3_600),
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 new_rejects_zero_from_address() {
let err = OrderCreation::new(
&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 new_rejects_app_data_digest_mismatch() {
let err = OrderCreation::new(
&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::new(
&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::error::VerifyOwnerError::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::new(
&order_data,
signature,
impostor,
EMPTY_APP_DATA_JSON.to_owned(),
None,
)
.unwrap();
let err = creation.verify_owner(&domain).unwrap_err();
match err {
crate::error::VerifyOwnerError::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::new(
&order_data,
signature,
owner,
EMPTY_APP_DATA_JSON.to_owned(),
None,
)
.unwrap();
assert_eq!(creation.verify_owner(&domain).unwrap(), owner);
}
#[test]
fn verify_owner_wraps_signature_recovery_failure() {
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::Eip712,
signature: zero_eip712_signature(),
from: address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"),
quote_id: None,
};
let err = creation
.verify_owner(&DomainSeparator::default())
.unwrap_err();
assert!(
matches!(err, crate::error::VerifyOwnerError::Signature(_)),
"got: {err:?}"
);
assert!(
matches!(
Error::from(err),
Error::VerifyOwner(crate::error::VerifyOwnerError::Signature(_))
),
"signature failure must survive lifting into Error::VerifyOwner"
);
}
#[test]
#[allow(deprecated)]
fn from_signed_order_data_delegates_to_new() {
let owner = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8");
let via_new = OrderCreation::new(
&empty_app_data_order(),
zero_eip712_signature(),
owner,
EMPTY_APP_DATA_JSON.to_owned(),
Some(42),
)
.unwrap();
let via_alias = OrderCreation::from_signed_order_data(
&empty_app_data_order(),
zero_eip712_signature(),
owner,
EMPTY_APP_DATA_JSON.to_owned(),
Some(42),
)
.unwrap();
assert_eq!(
serde_json::to_value(&via_new).unwrap(),
serde_json::to_value(&via_alias).unwrap()
);
}
#[test]
fn builder_matches_new() {
let owner = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8");
let via_new = OrderCreation::new(
&empty_app_data_order(),
zero_eip712_signature(),
owner,
EMPTY_APP_DATA_JSON.to_owned(),
Some(7),
)
.unwrap();
let via_builder = OrderCreation::builder(
&empty_app_data_order(),
zero_eip712_signature(),
owner,
EMPTY_APP_DATA_JSON.to_owned(),
)
.with_quote_id(7)
.build()
.unwrap();
assert_eq!(
serde_json::to_value(&via_new).unwrap(),
serde_json::to_value(&via_builder).unwrap()
);
}
#[test]
fn builder_build_surfaces_new_validation_error() {
let err = OrderCreation::builder(
&empty_app_data_order(),
zero_eip712_signature(),
Address::ZERO,
EMPTY_APP_DATA_JSON.to_owned(),
)
.build()
.unwrap_err();
assert!(
matches!(err, Error::OrderCreationInvalid { field: "from", .. }),
"got: {err}"
);
}
#[test]
fn builder_defaults_quote_id_to_none() {
let owner = address!("70997970C51812dc3A010C7d01b50e0d17dc79C8");
let creation = OrderCreation::builder(
&empty_app_data_order(),
zero_eip712_signature(),
owner,
EMPTY_APP_DATA_JSON.to_owned(),
)
.build()
.unwrap();
assert_eq!(creation.quote_id, None);
}
#[test]
fn new_rejects_valid_to_beyond_default_horizon() {
let mut order = empty_app_data_order();
order.valid_to = valid_to_after_for_tests(DEFAULT_ORDER_VALID_TO_MAX_HORIZON_SECS + 60);
let err = OrderCreation::new(
&order,
zero_eip712_signature(),
address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"),
EMPTY_APP_DATA_JSON.to_owned(),
None,
)
.unwrap_err();
assert!(
matches!(
err,
Error::OrderCreationInvalid {
field: "valid_to",
..
}
),
"got: {err}"
);
}
#[test]
fn builder_can_override_valid_to_horizon() {
let mut order = empty_app_data_order();
order.valid_to = valid_to_after_for_tests(DEFAULT_ORDER_VALID_TO_MAX_HORIZON_SECS + 60);
let creation = OrderCreation::builder(
&order,
zero_eip712_signature(),
address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"),
EMPTY_APP_DATA_JSON.to_owned(),
)
.with_valid_to_max_horizon_secs(DEFAULT_ORDER_VALID_TO_MAX_HORIZON_SECS + 120)
.build()
.unwrap();
assert_eq!(creation.valid_to, order.valid_to);
}
#[test]
fn presign_orders_are_exempt_from_valid_to_max_horizon() {
let mut order = empty_app_data_order();
order.valid_to = u32::MAX;
let creation = OrderCreation::new(
&order,
Signature::PreSign,
address!("70997970C51812dc3A010C7d01b50e0d17dc79C8"),
EMPTY_APP_DATA_JSON.to_owned(),
None,
)
.unwrap();
assert_eq!(creation.valid_to, u32::MAX);
}
}