use std::fmt;
use alloy_primitives::U256;
use cow_errors::CowError;
use cow_types::{EcdsaSigningScheme, OrderKind, PriceQuality, SigningScheme, TokenBalance};
use foldhash::HashMap;
use serde::{Deserialize, Serialize};
pub use cow_types::OnchainOrderData;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderQuoteRequest {
pub sell_token: alloy_primitives::Address,
pub buy_token: alloy_primitives::Address,
pub receiver: Option<alloy_primitives::Address>,
pub valid_to: Option<u32>,
pub app_data: String,
pub partially_fillable: bool,
pub sell_token_balance: TokenBalance,
pub buy_token_balance: TokenBalance,
pub from: alloy_primitives::Address,
pub price_quality: PriceQuality,
pub signing_scheme: EcdsaSigningScheme,
#[serde(flatten)]
pub side: QuoteSide,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QuoteSide {
pub kind: OrderKind,
#[serde(skip_serializing_if = "Option::is_none")]
pub sell_amount_before_fee: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub buy_amount_after_fee: Option<String>,
}
impl QuoteSide {
#[must_use]
pub fn sell(amount: impl ToString) -> Self {
Self {
kind: OrderKind::Sell,
sell_amount_before_fee: Some(amount.to_string()),
buy_amount_after_fee: None,
}
}
#[must_use]
pub fn buy(amount: impl ToString) -> Self {
Self {
kind: OrderKind::Buy,
sell_amount_before_fee: None,
buy_amount_after_fee: Some(amount.to_string()),
}
}
#[must_use]
pub const fn is_sell(&self) -> bool {
matches!(self.kind, OrderKind::Sell)
}
#[must_use]
pub const fn is_buy(&self) -> bool {
matches!(self.kind, OrderKind::Buy)
}
}
impl fmt::Display for QuoteSide {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
crate::types::OrderKind::Sell => {
let amt = self.sell_amount_before_fee.as_deref().map_or("?", |s| s);
write!(f, "sell {amt}")
}
crate::types::OrderKind::Buy => {
let amt = self.buy_amount_after_fee.as_deref().map_or("?", |s| s);
write!(f, "buy {amt}")
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderQuoteResponse {
pub quote: QuoteData,
pub from: alloy_primitives::Address,
pub expiration: String,
pub id: Option<i64>,
pub verified: bool,
#[serde(default)]
pub protocol_fee_bps: Option<String>,
}
impl OrderQuoteResponse {
#[must_use]
pub const fn has_id(&self) -> bool {
self.id.is_some()
}
#[must_use]
pub const fn has_protocol_fee_bps(&self) -> bool {
self.protocol_fee_bps.is_some()
}
#[must_use]
pub const fn is_verified(&self) -> bool {
self.verified
}
}
impl fmt::Display for OrderQuoteResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "quote-resp({})", self.quote)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QuoteData {
pub sell_token: alloy_primitives::Address,
pub buy_token: alloy_primitives::Address,
pub receiver: Option<alloy_primitives::Address>,
pub sell_amount: String,
pub buy_amount: String,
pub valid_to: u32,
pub app_data: String,
pub fee_amount: String,
pub kind: OrderKind,
pub partially_fillable: bool,
pub sell_token_balance: TokenBalance,
pub buy_token_balance: TokenBalance,
}
impl fmt::Display for QuoteData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"quote({} {:#x} sell={} → buy={})",
self.kind, self.sell_token, self.sell_amount, self.buy_amount
)
}
}
impl QuoteData {
#[must_use]
pub const fn is_sell(&self) -> bool {
self.kind.is_sell()
}
#[must_use]
pub const fn is_buy(&self) -> bool {
self.kind.is_buy()
}
#[must_use]
pub const fn is_partially_fillable(&self) -> bool {
self.partially_fillable
}
#[must_use]
pub const fn has_receiver(&self) -> bool {
self.receiver.is_some()
}
}
impl OrderQuoteRequest {
#[must_use]
pub fn new(
sell_token: alloy_primitives::Address,
buy_token: alloy_primitives::Address,
from: alloy_primitives::Address,
side: QuoteSide,
) -> Self {
Self {
sell_token,
buy_token,
from,
side,
receiver: None,
valid_to: None,
app_data: "0x0000000000000000000000000000000000000000000000000000000000000000"
.to_owned(),
partially_fillable: false,
sell_token_balance: TokenBalance::Erc20,
buy_token_balance: TokenBalance::Erc20,
price_quality: PriceQuality::Optimal,
signing_scheme: EcdsaSigningScheme::Eip712,
}
}
#[must_use]
pub const fn with_receiver(mut self, receiver: alloy_primitives::Address) -> Self {
self.receiver = Some(receiver);
self
}
#[must_use]
pub const fn with_valid_to(mut self, valid_to: u32) -> Self {
self.valid_to = Some(valid_to);
self
}
#[must_use]
pub fn with_app_data(mut self, app_data: impl Into<String>) -> Self {
self.app_data = app_data.into();
self
}
#[must_use]
pub const fn with_partially_fillable(mut self) -> Self {
self.partially_fillable = true;
self
}
#[must_use]
pub const fn with_price_quality(mut self, quality: PriceQuality) -> Self {
self.price_quality = quality;
self
}
#[must_use]
pub const fn with_sell_token_balance(mut self, balance: TokenBalance) -> Self {
self.sell_token_balance = balance;
self
}
#[must_use]
pub const fn with_buy_token_balance(mut self, balance: TokenBalance) -> Self {
self.buy_token_balance = balance;
self
}
#[must_use]
pub const fn with_signing_scheme(mut self, scheme: EcdsaSigningScheme) -> Self {
self.signing_scheme = scheme;
self
}
#[must_use]
pub const fn has_receiver(&self) -> bool {
self.receiver.is_some()
}
#[must_use]
pub const fn has_valid_to(&self) -> bool {
self.valid_to.is_some()
}
#[must_use]
pub const fn is_sell(&self) -> bool {
self.side.is_sell()
}
#[must_use]
pub const fn is_buy(&self) -> bool {
self.side.is_buy()
}
#[must_use]
pub const fn is_partially_fillable(&self) -> bool {
self.partially_fillable
}
}
impl fmt::Display for OrderQuoteRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "quote-req({:#x} → {:#x}, {})", self.sell_token, self.buy_token, self.side)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EthflowData {
pub user_valid_to: u32,
pub is_refund_claimed: bool,
}
impl EthflowData {
#[must_use]
pub const fn new(user_valid_to: u32, is_refund_claimed: bool) -> Self {
Self { user_valid_to, is_refund_claimed }
}
}
impl fmt::Display for EthflowData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ethflow(valid_to={}, refunded={})", self.user_valid_to, self.is_refund_claimed)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderCreation {
pub sell_token: alloy_primitives::Address,
pub buy_token: alloy_primitives::Address,
pub receiver: alloy_primitives::Address,
pub sell_amount: String,
pub buy_amount: String,
pub valid_to: u32,
pub app_data: String,
pub fee_amount: String,
pub kind: OrderKind,
pub partially_fillable: bool,
pub sell_token_balance: TokenBalance,
pub buy_token_balance: TokenBalance,
pub signing_scheme: SigningScheme,
pub signature: String,
pub from: alloy_primitives::Address,
#[serde(skip_serializing_if = "Option::is_none")]
pub quote_id: Option<i64>,
}
impl OrderCreation {
#[must_use]
pub fn from_quote(
quote: &QuoteData,
from: alloy_primitives::Address,
receiver: alloy_primitives::Address,
signing_scheme: SigningScheme,
signature: impl Into<String>,
) -> Self {
let effective_receiver = if receiver.is_zero() { from } else { receiver };
Self {
sell_token: quote.sell_token,
buy_token: quote.buy_token,
receiver: effective_receiver,
sell_amount: quote.sell_amount.clone(),
buy_amount: quote.buy_amount.clone(),
valid_to: quote.valid_to,
app_data: quote.app_data.clone(),
fee_amount: quote.fee_amount.clone(),
kind: quote.kind,
partially_fillable: quote.partially_fillable,
sell_token_balance: quote.sell_token_balance,
buy_token_balance: quote.buy_token_balance,
signing_scheme,
signature: signature.into(),
from,
quote_id: None,
}
}
#[must_use]
pub fn from_unsigned_order(
order: &cow_types::UnsignedOrder,
from: alloy_primitives::Address,
receiver: alloy_primitives::Address,
signing: cow_signing::types::SigningResult,
) -> Self {
let effective_receiver = if receiver.is_zero() { from } else { receiver };
Self {
sell_token: order.sell_token,
buy_token: order.buy_token,
receiver: effective_receiver,
sell_amount: order.sell_amount.to_string(),
buy_amount: order.buy_amount.to_string(),
valid_to: order.valid_to,
app_data: format!("{:#x}", order.app_data),
fee_amount: order.fee_amount.to_string(),
kind: order.kind,
partially_fillable: order.partially_fillable,
sell_token_balance: order.sell_token_balance,
buy_token_balance: order.buy_token_balance,
signing_scheme: signing.signing_scheme,
signature: signing.signature,
from,
quote_id: None,
}
}
#[must_use]
pub const fn with_sell_token_balance(mut self, balance: TokenBalance) -> Self {
self.sell_token_balance = balance;
self
}
#[must_use]
pub const fn with_buy_token_balance(mut self, balance: TokenBalance) -> Self {
self.buy_token_balance = balance;
self
}
#[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 has_quote_id(&self) -> bool {
self.quote_id.is_some()
}
#[must_use]
pub const fn is_sell(&self) -> bool {
self.kind.is_sell()
}
#[must_use]
pub const fn is_buy(&self) -> bool {
self.kind.is_buy()
}
#[must_use]
pub const fn is_partially_fillable(&self) -> bool {
self.partially_fillable
}
}
impl fmt::Display for OrderCreation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"order-creation({} {:#x} \u{2192} {:#x})",
self.kind, self.sell_token, self.buy_token
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum OrderStatus {
PresignaturePending,
Open,
Fulfilled,
Cancelled,
Expired,
}
impl OrderStatus {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::PresignaturePending => "presignaturePending",
Self::Open => "open",
Self::Fulfilled => "fulfilled",
Self::Cancelled => "cancelled",
Self::Expired => "expired",
}
}
#[must_use]
pub const fn is_pending(self) -> bool {
matches!(self, Self::PresignaturePending | Self::Open)
}
#[must_use]
pub const fn is_fulfilled(self) -> bool {
matches!(self, Self::Fulfilled)
}
#[must_use]
pub const fn is_cancelled(self) -> bool {
matches!(self, Self::Cancelled)
}
#[must_use]
pub const fn is_expired(self) -> bool {
matches!(self, Self::Expired)
}
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Fulfilled | Self::Cancelled | Self::Expired)
}
}
impl fmt::Display for OrderStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl TryFrom<&str> for OrderStatus {
type Error = CowError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"presignaturePending" => Ok(Self::PresignaturePending),
"open" => Ok(Self::Open),
"fulfilled" => Ok(Self::Fulfilled),
"cancelled" => Ok(Self::Cancelled),
"expired" => Ok(Self::Expired),
other => Err(CowError::Parse {
field: "OrderStatus",
reason: format!("unknown value: {other}"),
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum OrderClass {
Market,
Limit,
Liquidity,
}
impl OrderClass {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Market => "market",
Self::Limit => "limit",
Self::Liquidity => "liquidity",
}
}
#[must_use]
pub const fn is_market(self) -> bool {
matches!(self, Self::Market)
}
#[must_use]
pub const fn is_limit(self) -> bool {
matches!(self, Self::Limit)
}
#[must_use]
pub const fn is_liquidity(self) -> bool {
matches!(self, Self::Liquidity)
}
}
impl fmt::Display for OrderClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl TryFrom<&str> for OrderClass {
type Error = CowError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"market" => Ok(Self::Market),
"limit" => Ok(Self::Limit),
"liquidity" => Ok(Self::Liquidity),
other => Err(CowError::Parse {
field: "OrderClass",
reason: format!("unknown value: {other}"),
}),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InteractionData {
pub target: alloy_primitives::Address,
pub value: String,
pub call_data: String,
}
impl InteractionData {
#[must_use]
pub fn new(target: alloy_primitives::Address, call_data: impl Into<String>) -> Self {
Self { target, value: "0".to_owned(), call_data: call_data.into() }
}
#[must_use]
pub fn with_value(mut self, value: impl Into<String>) -> Self {
self.value = value.into();
self
}
#[must_use]
pub fn has_value(&self) -> bool {
self.value != "0"
}
}
impl fmt::Display for InteractionData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "interaction(target={:#x})", self.target)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderInteractions {
#[serde(default)]
pub pre: Vec<InteractionData>,
#[serde(default)]
pub post: Vec<InteractionData>,
}
impl OrderInteractions {
#[must_use]
pub const fn new(pre: Vec<InteractionData>, post: Vec<InteractionData>) -> Self {
Self { pre, post }
}
#[must_use]
pub const fn has_pre(&self) -> bool {
!self.pre.is_empty()
}
#[must_use]
pub const fn has_post(&self) -> bool {
!self.post.is_empty()
}
#[must_use]
pub const fn total(&self) -> usize {
self.pre.len() + self.post.len()
}
#[must_use]
pub fn with_pre(mut self, pre: Vec<InteractionData>) -> Self {
self.pre = pre;
self
}
#[must_use]
pub fn with_post(mut self, post: Vec<InteractionData>) -> Self {
self.post = post;
self
}
pub fn add_pre(&mut self, interaction: InteractionData) {
self.pre.push(interaction);
}
pub fn add_post(&mut self, interaction: InteractionData) {
self.post.push(interaction);
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.pre.is_empty() && self.post.is_empty()
}
}
impl fmt::Display for OrderInteractions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "interactions(pre={}, post={})", self.pre.len(), self.post.len())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Order {
pub uid: String,
pub owner: alloy_primitives::Address,
pub creation_date: String,
pub status: OrderStatus,
pub class: Option<OrderClass>,
pub sell_token: alloy_primitives::Address,
pub buy_token: alloy_primitives::Address,
pub receiver: Option<alloy_primitives::Address>,
pub sell_amount: String,
pub buy_amount: String,
pub valid_to: u32,
pub app_data: String,
pub full_app_data: Option<String>,
pub fee_amount: String,
pub kind: OrderKind,
pub partially_fillable: bool,
pub executed_sell_amount: String,
pub executed_buy_amount: String,
pub executed_sell_amount_before_fees: String,
pub executed_fee_amount: String,
pub invalidated: bool,
pub is_liquidity_order: Option<bool>,
pub signing_scheme: SigningScheme,
pub signature: String,
pub interactions: Option<OrderInteractions>,
#[serde(default)]
pub total_fee: Option<String>,
#[serde(default)]
pub full_fee_amount: Option<String>,
#[serde(default)]
pub available_balance: Option<String>,
#[serde(default)]
pub quote_id: Option<i64>,
#[serde(default)]
pub executed_fee: Option<String>,
#[serde(default)]
pub ethflow_data: Option<EthflowData>,
#[serde(default)]
pub onchain_order_data: Option<OnchainOrderData>,
#[serde(default)]
pub onchain_user: Option<alloy_primitives::Address>,
}
impl Order {
#[must_use]
pub const fn is_sell(&self) -> bool {
self.kind.is_sell()
}
#[must_use]
pub const fn is_buy(&self) -> bool {
self.kind.is_buy()
}
#[must_use]
pub fn effective_receiver(&self) -> alloy_primitives::Address {
self.receiver.map_or(self.owner, |r| r)
}
#[must_use]
pub fn has_interactions(&self) -> bool {
self.interactions.as_ref().is_some_and(|i| !i.pre.is_empty() || !i.post.is_empty())
}
#[must_use]
pub const fn has_surplus(&self) -> bool {
self.total_fee.is_some()
}
#[must_use]
pub const fn has_executed_fee(&self) -> bool {
self.executed_fee.is_some()
}
#[must_use]
pub const fn has_available_balance(&self) -> bool {
self.available_balance.is_some()
}
#[must_use]
pub const fn is_invalidated(&self) -> bool {
self.invalidated
}
#[must_use]
pub const fn has_full_app_data(&self) -> bool {
self.full_app_data.is_some()
}
#[must_use]
pub const fn has_ethflow_data(&self) -> bool {
self.ethflow_data.is_some()
}
#[must_use]
pub const fn has_onchain_data(&self) -> bool {
self.onchain_order_data.is_some()
}
#[must_use]
pub const fn has_onchain_user(&self) -> bool {
self.onchain_user.is_some()
}
#[must_use]
pub const fn has_receiver(&self) -> bool {
self.receiver.is_some()
}
#[must_use]
pub const fn has_class(&self) -> bool {
self.class.is_some()
}
#[must_use]
pub const fn has_quote_id(&self) -> bool {
self.quote_id.is_some()
}
#[must_use]
pub const fn has_full_fee_amount(&self) -> bool {
self.full_fee_amount.is_some()
}
#[must_use]
pub const fn is_partially_fillable(&self) -> bool {
self.partially_fillable
}
#[must_use]
pub fn is_liquidity_order(&self) -> bool {
self.is_liquidity_order.is_some_and(|v| v)
}
#[must_use]
pub const fn is_eth_flow(&self) -> bool {
self.onchain_order_data.is_some()
}
#[must_use]
pub fn total_executed_fee(&self) -> Option<U256> {
let fee_amount: U256 = self.executed_fee_amount.parse().ok()?;
let extra: U256 =
self.executed_fee.as_deref().and_then(|s| s.parse().ok()).map_or(U256::ZERO, |v| v);
Some(fee_amount.saturating_add(extra))
}
#[must_use]
pub const fn transform_eth_flow(mut self, _chain_id: u64) -> Self {
if self.onchain_order_data.is_none() {
return self;
}
self.sell_token = cow_chains::NATIVE_CURRENCY_ADDRESS;
if let Some(user) = self.onchain_user {
self.owner = user;
}
self
}
}
#[must_use]
pub const fn is_eth_flow_order(order: &Order) -> bool {
order.onchain_order_data.is_some()
}
impl fmt::Display for Order {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let short_uid = if self.uid.len() > 10 { &self.uid[..10] } else { &self.uid };
write!(f, "order({short_uid}… {} {})", self.kind, self.status)
}
}
#[derive(Debug, Clone)]
pub struct GetOrdersRequest {
pub owner: alloy_primitives::Address,
pub offset: Option<u32>,
pub limit: Option<u32>,
}
impl GetOrdersRequest {
#[must_use]
pub const fn for_owner(owner: alloy_primitives::Address) -> Self {
Self { owner, offset: None, limit: None }
}
#[must_use]
pub const fn with_offset(mut self, offset: u32) -> Self {
self.offset = Some(offset);
self
}
#[must_use]
pub const fn with_limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub const fn has_offset(&self) -> bool {
self.offset.is_some()
}
#[must_use]
pub const fn has_limit(&self) -> bool {
self.limit.is_some()
}
}
impl fmt::Display for GetOrdersRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "orders(owner={:#x})", self.owner)
}
}
#[derive(Debug, Clone, Default)]
pub struct GetTradesRequest {
pub owner: Option<alloy_primitives::Address>,
pub order_uid: Option<String>,
pub offset: Option<u32>,
pub limit: Option<u32>,
}
impl GetTradesRequest {
#[must_use]
pub const fn for_owner(owner: alloy_primitives::Address) -> Self {
Self { owner: Some(owner), order_uid: None, offset: None, limit: None }
}
#[must_use]
pub fn for_order_uid(uid: impl Into<String>) -> Self {
Self { owner: None, order_uid: Some(uid.into()), offset: None, limit: None }
}
#[must_use]
pub const fn with_offset(mut self, offset: u32) -> Self {
self.offset = Some(offset);
self
}
#[must_use]
pub const fn with_limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub const fn has_owner(&self) -> bool {
self.owner.is_some()
}
#[must_use]
pub const fn has_order_uid(&self) -> bool {
self.order_uid.is_some()
}
#[must_use]
pub const fn has_offset(&self) -> bool {
self.offset.is_some()
}
#[must_use]
pub const fn has_limit(&self) -> bool {
self.limit.is_some()
}
}
impl fmt::Display for GetTradesRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(uid) = &self.order_uid {
write!(f, "trades(uid={uid})")
} else if let Some(owner) = self.owner {
write!(f, "trades(owner={owner:#x})")
} else {
f.write_str("trades(all)")
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderUid(pub String);
impl OrderUid {
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub const fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl std::fmt::Display for OrderUid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for OrderUid {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<OrderUid> for String {
fn from(uid: OrderUid) -> Self {
uid.0
}
}
impl From<&str> for OrderUid {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderCancellations {
pub order_uids: Vec<String>,
pub signature: String,
pub signing_scheme: EcdsaSigningScheme,
}
impl OrderCancellations {
#[must_use]
pub fn new(
order_uids: Vec<String>,
signature: impl Into<String>,
signing_scheme: EcdsaSigningScheme,
) -> Self {
Self { order_uids, signature: signature.into(), signing_scheme }
}
#[must_use]
pub const fn order_count(&self) -> usize {
self.order_uids.len()
}
}
impl fmt::Display for OrderCancellations {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "cancel({} orders)", self.order_uids.len())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Trade {
pub block_number: u64,
pub log_index: u64,
pub order_uid: String,
pub owner: String,
pub sell_token: String,
pub buy_token: String,
pub sell_amount: String,
pub sell_amount_before_fees: String,
pub buy_amount: String,
pub tx_hash: Option<String>,
}
impl Trade {
#[must_use]
pub const fn has_tx_hash(&self) -> bool {
self.tx_hash.is_some()
}
}
impl fmt::Display for Trade {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let short_uid =
if self.order_uid.len() > 10 { &self.order_uid[..10] } else { &self.order_uid };
write!(f, "trade({short_uid}… block={})", self.block_number)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Auction {
pub id: Option<i64>,
pub block: u64,
pub orders: Vec<Order>,
pub prices: HashMap<String, String>,
}
impl Auction {
#[must_use]
pub const fn len(&self) -> usize {
self.orders.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.orders.is_empty()
}
#[must_use]
pub fn has_prices(&self) -> bool {
!self.prices.is_empty()
}
#[must_use]
pub fn get_price(&self, token: &str) -> Option<&str> {
self.prices.get(token).map(String::as_str)
}
#[must_use]
pub fn order_at(&self, index: usize) -> Option<&Order> {
self.orders.get(index)
}
#[must_use]
pub fn find_order_by_uid(&self, uid: &str) -> Option<&Order> {
self.orders.iter().find(|o| o.uid == uid)
}
}
impl fmt::Display for Auction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id = self.id.map_or(-1_i64, |i| i);
write!(f, "auction({id}, {} orders, block={})", self.orders.len(), self.block)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompetitionAuction {
pub orders: Vec<String>,
pub prices: HashMap<String, String>,
}
impl CompetitionAuction {
#[must_use]
pub const fn has_orders(&self) -> bool {
!self.orders.is_empty()
}
#[must_use]
pub fn get_price(&self, token: &str) -> Option<&str> {
self.prices.get(token).map(String::as_str)
}
#[must_use]
pub const fn len(&self) -> usize {
self.orders.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.orders.is_empty()
}
#[must_use]
pub fn has_prices(&self) -> bool {
!self.prices.is_empty()
}
}
impl fmt::Display for CompetitionAuction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "comp-auction({} orders)", self.orders.len())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SolverSettlement {
pub ranking: Option<f64>,
pub solver_address: Option<String>,
pub score: Option<String>,
pub reference_score: Option<String>,
pub tx_hash: Option<String>,
pub clearing_prices: Option<HashMap<String, String>>,
pub is_winner: Option<bool>,
pub filtered_out: Option<bool>,
}
impl SolverSettlement {
#[must_use]
pub fn is_winner(&self) -> bool {
self.is_winner.is_some_and(|w| w)
}
#[must_use]
pub const fn has_ranking(&self) -> bool {
self.ranking.is_some()
}
#[must_use]
pub const fn has_solver_address(&self) -> bool {
self.solver_address.is_some()
}
#[must_use]
pub const fn has_score(&self) -> bool {
self.score.is_some()
}
#[must_use]
pub const fn has_reference_score(&self) -> bool {
self.reference_score.is_some()
}
#[must_use]
pub const fn has_tx_hash(&self) -> bool {
self.tx_hash.is_some()
}
#[must_use]
pub const fn has_clearing_prices(&self) -> bool {
self.clearing_prices.is_some()
}
#[must_use]
pub fn is_filtered_out(&self) -> bool {
self.filtered_out.is_some_and(|f| f)
}
#[must_use]
pub fn get_clearing_price(&self, token: &str) -> Option<&str> {
self.clearing_prices.as_ref()?.get(token).map(String::as_str)
}
}
impl fmt::Display for SolverSettlement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let rank = self.ranking.map_or_else(|| "?".to_owned(), |r| r.to_string());
write!(f, "settlement(rank={rank})")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SolverCompetition {
pub auction_id: Option<i64>,
pub auction_start_block: Option<u64>,
pub auction_deadline_block: Option<u64>,
pub transaction_hashes: Option<Vec<String>>,
pub auction: Option<CompetitionAuction>,
pub solutions: Option<Vec<SolverSettlement>>,
}
impl SolverCompetition {
#[must_use]
pub const fn has_auction_id(&self) -> bool {
self.auction_id.is_some()
}
#[must_use]
pub const fn has_start_block(&self) -> bool {
self.auction_start_block.is_some()
}
#[must_use]
pub const fn has_deadline_block(&self) -> bool {
self.auction_deadline_block.is_some()
}
#[must_use]
pub fn is_settled(&self) -> bool {
self.transaction_hashes.as_ref().is_some_and(|v| !v.is_empty())
}
#[must_use]
pub const fn has_auction(&self) -> bool {
self.auction.is_some()
}
#[must_use]
pub const fn has_solutions(&self) -> bool {
self.solutions.is_some()
}
#[must_use]
pub fn num_solutions(&self) -> usize {
self.solutions.as_ref().map_or(0, Vec::len)
}
#[must_use]
pub fn winning_solution(&self) -> Option<&SolverSettlement> {
self.solutions.as_ref()?.iter().find(|s| s.is_winner())
}
#[must_use]
pub const fn has_transaction_hashes(&self) -> bool {
self.transaction_hashes.is_some()
}
}
impl fmt::Display for SolverCompetition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id = self.auction_id.map_or(-1_i64, |i| i);
write!(f, "competition(auction={id})")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CompetitionOrderStatusKind {
Open,
Scheduled,
Active,
Solved,
Executing,
Traded,
Cancelled,
}
impl CompetitionOrderStatusKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Open => "open",
Self::Scheduled => "scheduled",
Self::Active => "active",
Self::Solved => "solved",
Self::Executing => "executing",
Self::Traded => "traded",
Self::Cancelled => "cancelled",
}
}
#[must_use]
pub const fn is_open(self) -> bool {
matches!(self, Self::Open)
}
#[must_use]
pub const fn is_scheduled(self) -> bool {
matches!(self, Self::Scheduled)
}
#[must_use]
pub const fn is_active(self) -> bool {
matches!(self, Self::Active)
}
#[must_use]
pub const fn is_solved(self) -> bool {
matches!(self, Self::Solved)
}
#[must_use]
pub const fn is_executing(self) -> bool {
matches!(self, Self::Executing)
}
#[must_use]
pub const fn is_traded(self) -> bool {
matches!(self, Self::Traded)
}
#[must_use]
pub const fn is_cancelled(self) -> bool {
matches!(self, Self::Cancelled)
}
#[must_use]
pub const fn is_terminal(self) -> bool {
matches!(self, Self::Traded | Self::Cancelled)
}
#[must_use]
pub const fn is_pending(self) -> bool {
!self.is_terminal()
}
}
impl fmt::Display for CompetitionOrderStatusKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl TryFrom<&str> for CompetitionOrderStatusKind {
type Error = CowError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"open" => Ok(Self::Open),
"scheduled" => Ok(Self::Scheduled),
"active" => Ok(Self::Active),
"solved" => Ok(Self::Solved),
"executing" => Ok(Self::Executing),
"traded" => Ok(Self::Traded),
"cancelled" => Ok(Self::Cancelled),
other => Err(CowError::Parse {
field: "CompetitionOrderStatusKind",
reason: format!("unknown value: {other}"),
}),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SolverExecution {
pub solver: String,
pub executed_sell_amount: Option<String>,
pub executed_buy_amount: Option<String>,
}
impl SolverExecution {
#[must_use]
pub const fn has_executed_sell_amount(&self) -> bool {
self.executed_sell_amount.is_some()
}
#[must_use]
pub const fn has_executed_buy_amount(&self) -> bool {
self.executed_buy_amount.is_some()
}
#[must_use]
pub const fn both_amounts_available(&self) -> bool {
self.executed_sell_amount.is_some() && self.executed_buy_amount.is_some()
}
}
impl fmt::Display for SolverExecution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "exec({})", self.solver)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompetitionOrderStatus {
#[serde(rename = "type")]
pub kind: CompetitionOrderStatusKind,
pub value: Option<Vec<SolverExecution>>,
}
impl CompetitionOrderStatus {
#[must_use]
pub const fn has_value(&self) -> bool {
self.value.is_some()
}
#[must_use]
pub fn value_len(&self) -> usize {
self.value.as_ref().map_or(0, Vec::len)
}
}
impl fmt::Display for CompetitionOrderStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.kind, f)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TotalSurplus {
pub total_surplus: String,
}
impl TotalSurplus {
#[must_use]
pub fn new(total_surplus: impl Into<String>) -> Self {
Self { total_surplus: total_surplus.into() }
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.total_surplus
}
}
impl fmt::Display for TotalSurplus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "surplus({})", self.total_surplus)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppDataObject {
pub full_app_data: String,
}
impl AppDataObject {
#[must_use]
pub fn new(full_app_data: impl Into<String>) -> Self {
Self { full_app_data: full_app_data.into() }
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.full_app_data
}
#[must_use]
pub const fn len(&self) -> usize {
self.full_app_data.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.full_app_data.is_empty()
}
}
impl From<String> for AppDataObject {
fn from(s: String) -> Self {
Self { full_app_data: s }
}
}
impl From<AppDataObject> for String {
fn from(a: AppDataObject) -> Self {
a.full_app_data
}
}
impl fmt::Display for AppDataObject {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = &self.full_app_data;
let short = if s.len() > 20 { &s[..20] } else { s };
write!(f, "app-data({short}\u{2026})")
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::Address;
use super::*;
fn minimal_order() -> Order {
let json = serde_json::json!({
"uid": "0xabc123def456",
"owner": "0x0000000000000000000000000000000000000001",
"creationDate": "2024-01-01T00:00:00Z",
"status": "open",
"sellToken": "0x0000000000000000000000000000000000000002",
"buyToken": "0x0000000000000000000000000000000000000003",
"sellAmount": "1000000",
"buyAmount": "990000",
"validTo": 1_700_000_000_u32,
"appData": "0x0000000000000000000000000000000000000000000000000000000000000000",
"feeAmount": "1000",
"kind": "sell",
"partiallyFillable": false,
"executedSellAmount": "500000",
"executedBuyAmount": "495000",
"executedSellAmountBeforeFees": "499000",
"executedFeeAmount": "1000",
"invalidated": false,
"signingScheme": "eip712",
"signature": "0xdeadbeef"
});
serde_json::from_value(json).expect("minimal Order should deserialize")
}
#[test]
fn quote_side_sell_constructor() {
let side = QuoteSide::sell("1000");
assert!(side.is_sell());
assert!(!side.is_buy());
assert_eq!(side.sell_amount_before_fee.as_deref(), Some("1000"));
assert!(side.buy_amount_after_fee.is_none());
}
#[test]
fn quote_side_buy_constructor() {
let side = QuoteSide::buy(500_u64);
assert!(side.is_buy());
assert!(!side.is_sell());
assert_eq!(side.buy_amount_after_fee.as_deref(), Some("500"));
assert!(side.sell_amount_before_fee.is_none());
}
#[test]
fn quote_side_display_sell() {
let side = QuoteSide::sell("42");
assert_eq!(side.to_string(), "sell 42");
}
#[test]
fn quote_side_display_buy() {
let side = QuoteSide::buy("99");
assert_eq!(side.to_string(), "buy 99");
}
#[test]
fn quote_side_display_sell_none_amount() {
let side = QuoteSide {
kind: OrderKind::Sell,
sell_amount_before_fee: None,
buy_amount_after_fee: None,
};
assert_eq!(side.to_string(), "sell ?");
}
#[test]
fn quote_side_serde_roundtrip() {
let side = QuoteSide::sell("1234");
let json = serde_json::to_string(&side).unwrap();
let back: QuoteSide = serde_json::from_str(&json).unwrap();
assert!(back.is_sell());
assert_eq!(back.sell_amount_before_fee.as_deref(), Some("1234"));
}
#[test]
fn order_quote_request_new_defaults() {
let req = OrderQuoteRequest::new(
Address::ZERO,
Address::ZERO,
Address::ZERO,
QuoteSide::sell("1"),
);
assert!(!req.has_receiver());
assert!(!req.has_valid_to());
assert!(!req.is_partially_fillable());
assert!(req.is_sell());
assert!(!req.is_buy());
}
#[test]
fn order_quote_request_builder_chain() {
let receiver = Address::repeat_byte(0x01);
let req = OrderQuoteRequest::new(
Address::ZERO,
Address::ZERO,
Address::ZERO,
QuoteSide::buy("500"),
)
.with_receiver(receiver)
.with_valid_to(1_700_000_000)
.with_app_data("0xdeadbeef")
.with_partially_fillable()
.with_price_quality(PriceQuality::Fast)
.with_sell_token_balance(TokenBalance::Internal)
.with_buy_token_balance(TokenBalance::Internal)
.with_signing_scheme(EcdsaSigningScheme::EthSign);
assert!(req.has_receiver());
assert!(req.has_valid_to());
assert!(req.is_partially_fillable());
assert!(req.is_buy());
assert!(!req.is_sell());
assert_eq!(req.receiver, Some(receiver));
assert_eq!(req.valid_to, Some(1_700_000_000));
assert_eq!(req.app_data, "0xdeadbeef");
}
#[test]
fn order_quote_request_display() {
let req = OrderQuoteRequest::new(
Address::ZERO,
Address::ZERO,
Address::ZERO,
QuoteSide::sell("100"),
);
let s = req.to_string();
assert!(s.starts_with("quote-req("));
assert!(s.contains("sell 100"));
}
#[test]
fn order_quote_request_serde_roundtrip() {
let req = OrderQuoteRequest::new(
Address::ZERO,
Address::ZERO,
Address::ZERO,
QuoteSide::sell("1000"),
);
let json = serde_json::to_string(&req).unwrap();
let back: OrderQuoteRequest = serde_json::from_str(&json).unwrap();
assert!(back.is_sell());
assert_eq!(back.side.sell_amount_before_fee.as_deref(), Some("1000"));
}
#[test]
fn order_quote_response_predicates_and_display() {
let json = serde_json::json!({
"quote": {
"sellToken": "0x0000000000000000000000000000000000000001",
"buyToken": "0x0000000000000000000000000000000000000002",
"sellAmount": "1000",
"buyAmount": "990",
"validTo": 1_700_000_000_u32,
"appData": "0x00",
"feeAmount": "10",
"kind": "sell",
"partiallyFillable": false,
"sellTokenBalance": "erc20",
"buyTokenBalance": "erc20"
},
"from": "0x0000000000000000000000000000000000000003",
"expiration": "2024-01-01T00:00:00Z",
"id": 42,
"verified": true
});
let resp: OrderQuoteResponse = serde_json::from_value(json).unwrap();
assert!(resp.has_id());
assert!(resp.is_verified());
assert!(!resp.has_protocol_fee_bps());
assert!(resp.to_string().starts_with("quote-resp("));
}
#[test]
fn order_quote_response_no_id() {
let json = serde_json::json!({
"quote": {
"sellToken": "0x0000000000000000000000000000000000000001",
"buyToken": "0x0000000000000000000000000000000000000002",
"sellAmount": "1000",
"buyAmount": "990",
"validTo": 1_700_000_000_u32,
"appData": "0x00",
"feeAmount": "10",
"kind": "buy",
"partiallyFillable": true,
"sellTokenBalance": "erc20",
"buyTokenBalance": "erc20"
},
"from": "0x0000000000000000000000000000000000000003",
"expiration": "2024-01-01T00:00:00Z",
"id": null,
"verified": false,
"protocolFeeBps": "50"
});
let resp: OrderQuoteResponse = serde_json::from_value(json).unwrap();
assert!(!resp.has_id());
assert!(!resp.is_verified());
assert!(resp.has_protocol_fee_bps());
}
#[test]
fn quote_data_predicates() {
let json = serde_json::json!({
"sellToken": "0x0000000000000000000000000000000000000001",
"buyToken": "0x0000000000000000000000000000000000000002",
"receiver": "0x0000000000000000000000000000000000000099",
"sellAmount": "1000",
"buyAmount": "990",
"validTo": 1_700_000_000_u32,
"appData": "0x00",
"feeAmount": "10",
"kind": "sell",
"partiallyFillable": true,
"sellTokenBalance": "erc20",
"buyTokenBalance": "erc20"
});
let qd: QuoteData = serde_json::from_value(json).unwrap();
assert!(qd.is_sell());
assert!(!qd.is_buy());
assert!(qd.is_partially_fillable());
assert!(qd.has_receiver());
assert!(qd.to_string().contains("sell=1000"));
}
#[test]
fn quote_data_no_receiver() {
let json = serde_json::json!({
"sellToken": "0x0000000000000000000000000000000000000001",
"buyToken": "0x0000000000000000000000000000000000000002",
"sellAmount": "1000",
"buyAmount": "990",
"validTo": 1_700_000_000_u32,
"appData": "0x00",
"feeAmount": "10",
"kind": "buy",
"partiallyFillable": false,
"sellTokenBalance": "erc20",
"buyTokenBalance": "erc20"
});
let qd: QuoteData = serde_json::from_value(json).unwrap();
assert!(!qd.has_receiver());
assert!(qd.is_buy());
assert!(!qd.is_partially_fillable());
}
#[test]
fn ethflow_data_new_and_display() {
let data = EthflowData::new(1_700_000_000, false);
assert_eq!(data.user_valid_to, 1_700_000_000);
assert!(!data.is_refund_claimed);
assert!(data.to_string().contains("valid_to=1700000000"));
assert!(data.to_string().contains("refunded=false"));
}
#[test]
fn ethflow_data_serde_roundtrip() {
let data = EthflowData::new(1_234_567, true);
let json = serde_json::to_string(&data).unwrap();
let back: EthflowData = serde_json::from_str(&json).unwrap();
assert_eq!(back.user_valid_to, 1_234_567);
assert!(back.is_refund_claimed);
}
#[test]
fn onchain_order_data_new_and_predicate() {
let data = OnchainOrderData::new(Address::ZERO);
assert!(!data.has_placement_error());
assert!(data.to_string().contains("onchain(sender="));
}
#[test]
fn onchain_order_data_serde_roundtrip() {
let data = OnchainOrderData::new(Address::repeat_byte(0xaa));
let json = serde_json::to_string(&data).unwrap();
let back: OnchainOrderData = serde_json::from_str(&json).unwrap();
assert!(!back.has_placement_error());
}
#[test]
fn order_creation_from_quote() {
let quote_json = serde_json::json!({
"sellToken": "0x0000000000000000000000000000000000000001",
"buyToken": "0x0000000000000000000000000000000000000002",
"sellAmount": "1000",
"buyAmount": "990",
"validTo": 1_700_000_000_u32,
"appData": "0x00",
"feeAmount": "10",
"kind": "sell",
"partiallyFillable": false,
"sellTokenBalance": "erc20",
"buyTokenBalance": "erc20"
});
let quote: QuoteData = serde_json::from_value(quote_json).unwrap();
let from = Address::repeat_byte(0x11);
let creation =
OrderCreation::from_quote("e, from, Address::ZERO, SigningScheme::Eip712, "0xsig");
assert_eq!(creation.receiver, from);
assert!(creation.is_sell());
assert!(!creation.is_buy());
assert!(!creation.has_quote_id());
assert!(!creation.is_partially_fillable());
}
#[test]
fn order_creation_builder_methods() {
let quote_json = serde_json::json!({
"sellToken": "0x0000000000000000000000000000000000000001",
"buyToken": "0x0000000000000000000000000000000000000002",
"sellAmount": "1000",
"buyAmount": "990",
"validTo": 1_700_000_000_u32,
"appData": "0x00",
"feeAmount": "10",
"kind": "buy",
"partiallyFillable": false,
"sellTokenBalance": "erc20",
"buyTokenBalance": "erc20"
});
let quote: QuoteData = serde_json::from_value(quote_json).unwrap();
let from = Address::repeat_byte(0x11);
let receiver = Address::repeat_byte(0x22);
let creation =
OrderCreation::from_quote("e, from, receiver, SigningScheme::Eip712, "0xsig")
.with_quote_id(42)
.with_sell_token_balance(TokenBalance::Internal)
.with_buy_token_balance(TokenBalance::Internal);
assert!(creation.has_quote_id());
assert_eq!(creation.receiver, receiver);
assert!(creation.is_buy());
}
#[test]
fn order_creation_display() {
let quote_json = serde_json::json!({
"sellToken": "0x0000000000000000000000000000000000000001",
"buyToken": "0x0000000000000000000000000000000000000002",
"sellAmount": "1000",
"buyAmount": "990",
"validTo": 1_700_000_000_u32,
"appData": "0x00",
"feeAmount": "10",
"kind": "sell",
"partiallyFillable": false,
"sellTokenBalance": "erc20",
"buyTokenBalance": "erc20"
});
let quote: QuoteData = serde_json::from_value(quote_json).unwrap();
let creation = OrderCreation::from_quote(
"e,
Address::ZERO,
Address::ZERO,
SigningScheme::Eip712,
"0xsig",
);
let s = creation.to_string();
assert!(s.starts_with("order-creation("));
}
#[test]
fn order_creation_serde_roundtrip() {
let quote_json = serde_json::json!({
"sellToken": "0x0000000000000000000000000000000000000001",
"buyToken": "0x0000000000000000000000000000000000000002",
"sellAmount": "1000",
"buyAmount": "990",
"validTo": 1_700_000_000_u32,
"appData": "0x00",
"feeAmount": "10",
"kind": "sell",
"partiallyFillable": false,
"sellTokenBalance": "erc20",
"buyTokenBalance": "erc20"
});
let quote: QuoteData = serde_json::from_value(quote_json).unwrap();
let creation = OrderCreation::from_quote(
"e,
Address::ZERO,
Address::ZERO,
SigningScheme::Eip712,
"0xsig",
);
let json = serde_json::to_string(&creation).unwrap();
let back: OrderCreation = serde_json::from_str(&json).unwrap();
assert_eq!(back.sell_amount, "1000");
assert!(back.is_sell());
}
#[test]
fn order_status_as_str() {
assert_eq!(OrderStatus::PresignaturePending.as_str(), "presignaturePending");
assert_eq!(OrderStatus::Open.as_str(), "open");
assert_eq!(OrderStatus::Fulfilled.as_str(), "fulfilled");
assert_eq!(OrderStatus::Cancelled.as_str(), "cancelled");
assert_eq!(OrderStatus::Expired.as_str(), "expired");
}
#[test]
fn order_status_predicates() {
assert!(OrderStatus::Open.is_pending());
assert!(OrderStatus::PresignaturePending.is_pending());
assert!(!OrderStatus::Fulfilled.is_pending());
assert!(OrderStatus::Fulfilled.is_fulfilled());
assert!(OrderStatus::Cancelled.is_cancelled());
assert!(OrderStatus::Expired.is_expired());
assert!(OrderStatus::Fulfilled.is_terminal());
assert!(OrderStatus::Cancelled.is_terminal());
assert!(OrderStatus::Expired.is_terminal());
assert!(!OrderStatus::Open.is_terminal());
}
#[test]
fn order_status_display() {
assert_eq!(OrderStatus::Open.to_string(), "open");
assert_eq!(OrderStatus::Fulfilled.to_string(), "fulfilled");
}
#[test]
fn order_status_try_from_str() {
assert_eq!(OrderStatus::try_from("open").unwrap(), OrderStatus::Open);
assert_eq!(OrderStatus::try_from("fulfilled").unwrap(), OrderStatus::Fulfilled);
assert_eq!(OrderStatus::try_from("cancelled").unwrap(), OrderStatus::Cancelled);
assert_eq!(OrderStatus::try_from("expired").unwrap(), OrderStatus::Expired);
assert_eq!(
OrderStatus::try_from("presignaturePending").unwrap(),
OrderStatus::PresignaturePending
);
assert!(OrderStatus::try_from("bogus").is_err());
}
#[test]
fn order_status_serde_roundtrip() {
let status = OrderStatus::Fulfilled;
let json = serde_json::to_string(&status).unwrap();
assert_eq!(json, "\"fulfilled\"");
let back: OrderStatus = serde_json::from_str(&json).unwrap();
assert_eq!(back, OrderStatus::Fulfilled);
}
#[test]
fn order_class_as_str() {
assert_eq!(OrderClass::Market.as_str(), "market");
assert_eq!(OrderClass::Limit.as_str(), "limit");
assert_eq!(OrderClass::Liquidity.as_str(), "liquidity");
}
#[test]
fn order_class_predicates() {
assert!(OrderClass::Market.is_market());
assert!(!OrderClass::Market.is_limit());
assert!(OrderClass::Limit.is_limit());
assert!(OrderClass::Liquidity.is_liquidity());
}
#[test]
fn order_class_try_from_str() {
assert_eq!(OrderClass::try_from("market").unwrap(), OrderClass::Market);
assert_eq!(OrderClass::try_from("limit").unwrap(), OrderClass::Limit);
assert_eq!(OrderClass::try_from("liquidity").unwrap(), OrderClass::Liquidity);
assert!(OrderClass::try_from("unknown").is_err());
}
#[test]
fn order_class_serde_roundtrip() {
let class = OrderClass::Limit;
let json = serde_json::to_string(&class).unwrap();
assert_eq!(json, "\"limit\"");
let back: OrderClass = serde_json::from_str(&json).unwrap();
assert_eq!(back, OrderClass::Limit);
}
#[test]
fn interaction_data_new_defaults_value_zero() {
let interaction = InteractionData::new(Address::ZERO, "0xdeadbeef");
assert_eq!(interaction.value, "0");
assert!(!interaction.has_value());
assert_eq!(interaction.call_data, "0xdeadbeef");
}
#[test]
fn interaction_data_with_value() {
let interaction = InteractionData::new(Address::ZERO, "0xaa").with_value("1000");
assert!(interaction.has_value());
assert_eq!(interaction.value, "1000");
}
#[test]
fn interaction_data_display() {
let interaction = InteractionData::new(Address::ZERO, "0x");
assert!(interaction.to_string().starts_with("interaction(target="));
}
#[test]
fn interaction_data_serde_roundtrip() {
let interaction = InteractionData::new(Address::ZERO, "0xcafe").with_value("42");
let json = serde_json::to_string(&interaction).unwrap();
let back: InteractionData = serde_json::from_str(&json).unwrap();
assert_eq!(back.value, "42");
assert_eq!(back.call_data, "0xcafe");
}
#[test]
fn order_interactions_default_is_empty() {
let interactions = OrderInteractions::default();
assert!(interactions.is_empty());
assert!(!interactions.has_pre());
assert!(!interactions.has_post());
assert_eq!(interactions.total(), 0);
}
#[test]
fn order_interactions_add_hooks() {
let mut interactions = OrderInteractions::default();
interactions.add_pre(InteractionData::new(Address::ZERO, "0x01"));
interactions.add_post(InteractionData::new(Address::ZERO, "0x02"));
interactions.add_post(InteractionData::new(Address::ZERO, "0x03"));
assert!(!interactions.is_empty());
assert!(interactions.has_pre());
assert!(interactions.has_post());
assert_eq!(interactions.total(), 3);
}
#[test]
fn order_interactions_builder() {
let pre = vec![InteractionData::new(Address::ZERO, "0x01")];
let post = vec![InteractionData::new(Address::ZERO, "0x02")];
let interactions = OrderInteractions::default().with_pre(pre).with_post(post);
assert_eq!(interactions.total(), 2);
}
#[test]
fn order_interactions_new_constructor() {
let interactions =
OrderInteractions::new(vec![InteractionData::new(Address::ZERO, "0x01")], vec![]);
assert!(interactions.has_pre());
assert!(!interactions.has_post());
}
#[test]
fn order_interactions_display() {
let interactions = OrderInteractions::new(
vec![InteractionData::new(Address::ZERO, "0x01")],
vec![
InteractionData::new(Address::ZERO, "0x02"),
InteractionData::new(Address::ZERO, "0x03"),
],
);
assert_eq!(interactions.to_string(), "interactions(pre=1, post=2)");
}
#[test]
fn order_interactions_serde_roundtrip() {
let interactions =
OrderInteractions::new(vec![InteractionData::new(Address::ZERO, "0x01")], vec![]);
let json = serde_json::to_string(&interactions).unwrap();
let back: OrderInteractions = serde_json::from_str(&json).unwrap();
assert!(back.has_pre());
assert!(!back.has_post());
}
#[test]
fn order_deserializes_from_json() {
let order = minimal_order();
assert_eq!(order.uid, "0xabc123def456");
assert!(order.is_sell());
assert!(!order.is_buy());
assert!(!order.is_partially_fillable());
assert!(!order.is_invalidated());
}
#[test]
fn order_effective_receiver_falls_back_to_owner() {
let order = minimal_order();
assert!(!order.has_receiver());
assert_eq!(order.effective_receiver(), order.owner);
}
#[test]
fn order_effective_receiver_uses_receiver_when_set() {
let mut order = minimal_order();
let custom_receiver = Address::repeat_byte(0xff);
order.receiver = Some(custom_receiver);
assert!(order.has_receiver());
assert_eq!(order.effective_receiver(), custom_receiver);
}
#[test]
fn order_has_interactions_false_when_none() {
let order = minimal_order();
assert!(!order.has_interactions());
}
#[test]
fn order_has_interactions_false_when_empty() {
let mut order = minimal_order();
order.interactions = Some(OrderInteractions::default());
assert!(!order.has_interactions());
}
#[test]
fn order_has_interactions_true_when_hooks_present() {
let mut order = minimal_order();
order.interactions =
Some(OrderInteractions::new(vec![InteractionData::new(Address::ZERO, "0x01")], vec![]));
assert!(order.has_interactions());
}
#[test]
fn order_optional_field_predicates() {
let order = minimal_order();
assert!(!order.has_surplus());
assert!(!order.has_executed_fee());
assert!(!order.has_available_balance());
assert!(!order.has_full_app_data());
assert!(!order.has_ethflow_data());
assert!(!order.has_onchain_data());
assert!(!order.has_onchain_user());
assert!(!order.has_class());
assert!(!order.has_quote_id());
assert!(!order.has_full_fee_amount());
assert!(!order.is_eth_flow());
}
#[test]
fn order_total_executed_fee() {
let mut order = minimal_order();
order.executed_fee_amount = "1000".to_owned();
order.executed_fee = Some("500".to_owned());
let total = order.total_executed_fee().unwrap();
assert_eq!(total, U256::from(1500));
}
#[test]
fn order_total_executed_fee_without_extra() {
let mut order = minimal_order();
order.executed_fee_amount = "1000".to_owned();
order.executed_fee = None;
let total = order.total_executed_fee().unwrap();
assert_eq!(total, U256::from(1000));
}
#[test]
fn order_total_executed_fee_invalid_returns_none() {
let mut order = minimal_order();
order.executed_fee_amount = "not_a_number".to_owned();
assert!(order.total_executed_fee().is_none());
}
#[test]
fn order_is_liquidity_order_defaults_false() {
let order = minimal_order();
assert!(!order.is_liquidity_order());
}
#[test]
fn order_is_liquidity_order_when_true() {
let mut order = minimal_order();
order.is_liquidity_order = Some(true);
assert!(order.is_liquidity_order());
}
#[test]
fn order_is_liquidity_order_explicit_false() {
let mut order = minimal_order();
order.is_liquidity_order = Some(false);
assert!(!order.is_liquidity_order());
}
#[test]
fn order_transform_eth_flow_noop_for_regular_order() {
let order = minimal_order();
let sell_token = order.sell_token;
let transformed = order.transform_eth_flow(1);
assert_eq!(transformed.sell_token, sell_token);
}
#[test]
fn order_transform_eth_flow_replaces_sell_token_and_owner() {
let mut order = minimal_order();
let real_user = Address::repeat_byte(0xaa);
order.onchain_order_data = Some(OnchainOrderData::new(Address::ZERO));
order.onchain_user = Some(real_user);
let transformed = order.transform_eth_flow(1);
assert_eq!(transformed.sell_token, cow_chains::NATIVE_CURRENCY_ADDRESS);
assert_eq!(transformed.owner, real_user);
}
#[test]
fn order_display() {
let order = minimal_order();
let s = order.to_string();
assert!(s.starts_with("order("));
assert!(s.contains("sell"));
assert!(s.contains("open"));
}
#[test]
fn order_serde_roundtrip() {
let order = minimal_order();
let json = serde_json::to_string(&order).unwrap();
let back: Order = serde_json::from_str(&json).unwrap();
assert_eq!(back.uid, order.uid);
assert_eq!(back.sell_amount, order.sell_amount);
}
#[test]
fn is_eth_flow_order_free_function() {
let order = minimal_order();
assert!(!is_eth_flow_order(&order));
let mut order2 = minimal_order();
order2.onchain_order_data = Some(OnchainOrderData::new(Address::ZERO));
assert!(is_eth_flow_order(&order2));
}
#[test]
fn get_orders_request_for_owner() {
let req = GetOrdersRequest::for_owner(Address::ZERO);
assert!(!req.has_offset());
assert!(!req.has_limit());
}
#[test]
fn get_orders_request_builder() {
let req = GetOrdersRequest::for_owner(Address::ZERO).with_offset(10).with_limit(50);
assert!(req.has_offset());
assert!(req.has_limit());
assert_eq!(req.offset, Some(10));
assert_eq!(req.limit, Some(50));
}
#[test]
fn get_orders_request_display() {
let req = GetOrdersRequest::for_owner(Address::ZERO);
assert!(req.to_string().starts_with("orders(owner="));
}
#[test]
fn get_trades_request_default() {
let req = GetTradesRequest::default();
assert!(!req.has_owner());
assert!(!req.has_order_uid());
assert!(!req.has_offset());
assert!(!req.has_limit());
}
#[test]
fn get_trades_request_for_owner() {
let req = GetTradesRequest::for_owner(Address::ZERO);
assert!(req.has_owner());
assert!(!req.has_order_uid());
}
#[test]
fn get_trades_request_for_order_uid() {
let req = GetTradesRequest::for_order_uid("0xabc");
assert!(req.has_order_uid());
assert!(!req.has_owner());
}
#[test]
fn get_trades_request_pagination() {
let req = GetTradesRequest::for_owner(Address::ZERO).with_offset(5).with_limit(20);
assert!(req.has_offset());
assert!(req.has_limit());
}
#[test]
fn get_trades_request_display_by_uid() {
let req = GetTradesRequest::for_order_uid("0xabc");
assert!(req.to_string().contains("uid=0xabc"));
}
#[test]
fn get_trades_request_display_by_owner() {
let req = GetTradesRequest::for_owner(Address::ZERO);
assert!(req.to_string().contains("owner="));
}
#[test]
fn get_trades_request_display_all() {
let req = GetTradesRequest::default();
assert_eq!(req.to_string(), "trades(all)");
}
#[test]
fn order_uid_from_str() {
let uid = OrderUid::from("0xabc123");
assert_eq!(uid.as_str(), "0xabc123");
assert_eq!(uid.len(), 8);
assert!(!uid.is_empty());
}
#[test]
fn order_uid_from_string() {
let uid = OrderUid::from("hello".to_owned());
assert_eq!(uid.as_str(), "hello");
}
#[test]
fn order_uid_into_string() {
let uid = OrderUid::from("test");
let s: String = uid.into();
assert_eq!(s, "test");
}
#[test]
fn order_uid_empty() {
let uid = OrderUid::from("");
assert!(uid.is_empty());
assert_eq!(uid.len(), 0);
}
#[test]
fn order_uid_display() {
let uid = OrderUid::from("0xabc");
assert_eq!(uid.to_string(), "0xabc");
}
#[test]
fn order_uid_serde_roundtrip() {
let uid = OrderUid::from("0xdeadbeef");
let json = serde_json::to_string(&uid).unwrap();
let back: OrderUid = serde_json::from_str(&json).unwrap();
assert_eq!(back.as_str(), "0xdeadbeef");
}
#[test]
fn order_cancellations_new() {
let cancel = OrderCancellations::new(
vec!["0xabc".to_owned(), "0xdef".to_owned()],
"0xsig",
EcdsaSigningScheme::Eip712,
);
assert_eq!(cancel.order_count(), 2);
assert_eq!(cancel.signature, "0xsig");
}
#[test]
fn order_cancellations_display() {
let cancel =
OrderCancellations::new(vec!["0xa".to_owned()], "0xsig", EcdsaSigningScheme::Eip712);
assert_eq!(cancel.to_string(), "cancel(1 orders)");
}
#[test]
fn order_cancellations_serde_roundtrip() {
let cancel =
OrderCancellations::new(vec!["0xabc".to_owned()], "0xsig", EcdsaSigningScheme::Eip712);
let json = serde_json::to_string(&cancel).unwrap();
let back: OrderCancellations = serde_json::from_str(&json).unwrap();
assert_eq!(back.order_count(), 1);
}
#[test]
fn trade_deserialize_and_predicates() {
let json = serde_json::json!({
"blockNumber": 12345,
"logIndex": 0,
"orderUid": "0xabc123def456",
"owner": "0x0000000000000000000000000000000000000001",
"sellToken": "0x0000000000000000000000000000000000000002",
"buyToken": "0x0000000000000000000000000000000000000003",
"sellAmount": "1000",
"sellAmountBeforeFees": "990",
"buyAmount": "500",
"txHash": "0xdeadbeef"
});
let trade: Trade = serde_json::from_value(json).unwrap();
assert!(trade.has_tx_hash());
assert_eq!(trade.block_number, 12345);
}
#[test]
fn trade_without_tx_hash() {
let json = serde_json::json!({
"blockNumber": 100,
"logIndex": 1,
"orderUid": "0xabc",
"owner": "0x01",
"sellToken": "0x02",
"buyToken": "0x03",
"sellAmount": "1000",
"sellAmountBeforeFees": "990",
"buyAmount": "500",
"txHash": null
});
let trade: Trade = serde_json::from_value(json).unwrap();
assert!(!trade.has_tx_hash());
}
#[test]
fn trade_display() {
let json = serde_json::json!({
"blockNumber": 999,
"logIndex": 0,
"orderUid": "0xabc123def456",
"owner": "0x01",
"sellToken": "0x02",
"buyToken": "0x03",
"sellAmount": "1000",
"sellAmountBeforeFees": "990",
"buyAmount": "500",
"txHash": null
});
let trade: Trade = serde_json::from_value(json).unwrap();
assert!(trade.to_string().contains("block=999"));
}
#[test]
fn trade_serde_roundtrip() {
let json = serde_json::json!({
"blockNumber": 100,
"logIndex": 5,
"orderUid": "0xuid",
"owner": "0x01",
"sellToken": "0x02",
"buyToken": "0x03",
"sellAmount": "1000",
"sellAmountBeforeFees": "990",
"buyAmount": "500",
"txHash": "0xhash"
});
let trade: Trade = serde_json::from_value(json).unwrap();
let serialized = serde_json::to_string(&trade).unwrap();
let back: Trade = serde_json::from_str(&serialized).unwrap();
assert_eq!(back.block_number, 100);
assert_eq!(back.log_index, 5);
}
#[test]
fn competition_auction_empty() {
let auction = CompetitionAuction { orders: vec![], prices: HashMap::default() };
assert!(auction.is_empty());
assert!(!auction.has_orders());
assert!(!auction.has_prices());
assert_eq!(auction.len(), 0);
}
#[test]
fn competition_auction_with_data() {
let mut prices = HashMap::default();
prices.insert("0xtoken".to_owned(), "1000000".to_owned());
let auction = CompetitionAuction { orders: vec!["0xorder1".to_owned()], prices };
assert!(!auction.is_empty());
assert!(auction.has_orders());
assert!(auction.has_prices());
assert_eq!(auction.len(), 1);
assert_eq!(auction.get_price("0xtoken"), Some("1000000"));
assert!(auction.get_price("0xnonexistent").is_none());
}
#[test]
fn competition_auction_display() {
let auction = CompetitionAuction {
orders: vec!["a".to_owned(), "b".to_owned()],
prices: HashMap::default(),
};
assert_eq!(auction.to_string(), "comp-auction(2 orders)");
}
#[test]
fn solver_settlement_defaults_none() {
let json = serde_json::json!({});
let settlement: SolverSettlement = serde_json::from_value(json).unwrap();
assert!(!settlement.is_winner());
assert!(!settlement.has_ranking());
assert!(!settlement.has_solver_address());
assert!(!settlement.has_score());
assert!(!settlement.has_reference_score());
assert!(!settlement.has_tx_hash());
assert!(!settlement.has_clearing_prices());
assert!(!settlement.is_filtered_out());
assert!(settlement.get_clearing_price("0xtoken").is_none());
}
#[test]
fn solver_settlement_full() {
let mut clearing = HashMap::default();
clearing.insert("0xtoken".to_owned(), "999".to_owned());
let json = serde_json::json!({
"ranking": 1.0,
"solverAddress": "0x0000000000000000000000000000000000000001",
"score": "42",
"referenceScore": "40",
"txHash": "0xdeadbeef",
"clearingPrices": clearing,
"isWinner": true,
"filteredOut": false
});
let settlement: SolverSettlement = serde_json::from_value(json).unwrap();
assert!(settlement.is_winner());
assert!(settlement.has_ranking());
assert!(settlement.has_solver_address());
assert!(settlement.has_score());
assert!(settlement.has_reference_score());
assert!(settlement.has_tx_hash());
assert!(settlement.has_clearing_prices());
assert!(!settlement.is_filtered_out());
assert_eq!(settlement.get_clearing_price("0xtoken"), Some("999"));
assert!(settlement.get_clearing_price("0xother").is_none());
}
#[test]
fn solver_settlement_display() {
let json = serde_json::json!({"ranking": 3.0});
let settlement: SolverSettlement = serde_json::from_value(json).unwrap();
assert_eq!(settlement.to_string(), "settlement(rank=3)");
}
#[test]
fn solver_settlement_display_no_rank() {
let json = serde_json::json!({});
let settlement: SolverSettlement = serde_json::from_value(json).unwrap();
assert_eq!(settlement.to_string(), "settlement(rank=?)");
}
#[test]
fn solver_competition_empty() {
let json = serde_json::json!({});
let comp: SolverCompetition = serde_json::from_value(json).unwrap();
assert!(!comp.has_auction_id());
assert!(!comp.has_start_block());
assert!(!comp.has_deadline_block());
assert!(!comp.is_settled());
assert!(!comp.has_auction());
assert!(!comp.has_solutions());
assert!(!comp.has_transaction_hashes());
assert_eq!(comp.num_solutions(), 0);
assert!(comp.winning_solution().is_none());
}
#[test]
fn solver_competition_with_winner() {
let json = serde_json::json!({
"auctionId": 100,
"auctionStartBlock": 1000,
"auctionDeadlineBlock": 1010,
"transactionHashes": ["0xhash"],
"auction": {
"orders": ["0xorder1"],
"prices": {}
},
"solutions": [
{"isWinner": false},
{"isWinner": true, "ranking": 1.0}
]
});
let comp: SolverCompetition = serde_json::from_value(json).unwrap();
assert!(comp.has_auction_id());
assert!(comp.has_start_block());
assert!(comp.has_deadline_block());
assert!(comp.is_settled());
assert!(comp.has_auction());
assert!(comp.has_solutions());
assert_eq!(comp.num_solutions(), 2);
let winner = comp.winning_solution().unwrap();
assert!(winner.is_winner());
}
#[test]
fn solver_competition_not_settled_empty_hashes() {
let json = serde_json::json!({
"transactionHashes": []
});
let comp: SolverCompetition = serde_json::from_value(json).unwrap();
assert!(!comp.is_settled());
}
#[test]
fn solver_competition_display() {
let json = serde_json::json!({"auctionId": 42});
let comp: SolverCompetition = serde_json::from_value(json).unwrap();
assert_eq!(comp.to_string(), "competition(auction=42)");
}
#[test]
fn solver_competition_display_no_id() {
let json = serde_json::json!({});
let comp: SolverCompetition = serde_json::from_value(json).unwrap();
assert_eq!(comp.to_string(), "competition(auction=-1)");
}
#[test]
fn competition_order_status_kind_as_str() {
assert_eq!(CompetitionOrderStatusKind::Open.as_str(), "open");
assert_eq!(CompetitionOrderStatusKind::Scheduled.as_str(), "scheduled");
assert_eq!(CompetitionOrderStatusKind::Active.as_str(), "active");
assert_eq!(CompetitionOrderStatusKind::Solved.as_str(), "solved");
assert_eq!(CompetitionOrderStatusKind::Executing.as_str(), "executing");
assert_eq!(CompetitionOrderStatusKind::Traded.as_str(), "traded");
assert_eq!(CompetitionOrderStatusKind::Cancelled.as_str(), "cancelled");
}
#[test]
fn competition_order_status_kind_predicates() {
assert!(CompetitionOrderStatusKind::Open.is_open());
assert!(CompetitionOrderStatusKind::Scheduled.is_scheduled());
assert!(CompetitionOrderStatusKind::Active.is_active());
assert!(CompetitionOrderStatusKind::Solved.is_solved());
assert!(CompetitionOrderStatusKind::Executing.is_executing());
assert!(CompetitionOrderStatusKind::Traded.is_traded());
assert!(CompetitionOrderStatusKind::Cancelled.is_cancelled());
assert!(CompetitionOrderStatusKind::Traded.is_terminal());
assert!(CompetitionOrderStatusKind::Cancelled.is_terminal());
assert!(!CompetitionOrderStatusKind::Open.is_terminal());
assert!(CompetitionOrderStatusKind::Open.is_pending());
assert!(!CompetitionOrderStatusKind::Traded.is_pending());
}
#[test]
fn competition_order_status_kind_try_from() {
assert_eq!(
CompetitionOrderStatusKind::try_from("open").unwrap(),
CompetitionOrderStatusKind::Open
);
assert_eq!(
CompetitionOrderStatusKind::try_from("traded").unwrap(),
CompetitionOrderStatusKind::Traded
);
assert!(CompetitionOrderStatusKind::try_from("bogus").is_err());
}
#[test]
fn competition_order_status_kind_serde_roundtrip() {
let kind = CompetitionOrderStatusKind::Executing;
let json = serde_json::to_string(&kind).unwrap();
assert_eq!(json, "\"executing\"");
let back: CompetitionOrderStatusKind = serde_json::from_str(&json).unwrap();
assert_eq!(back, CompetitionOrderStatusKind::Executing);
}
#[test]
fn solver_execution_predicates() {
let json = serde_json::json!({
"solver": "test_solver",
"executedSellAmount": "1000",
"executedBuyAmount": "500"
});
let exec: SolverExecution = serde_json::from_value(json).unwrap();
assert!(exec.has_executed_sell_amount());
assert!(exec.has_executed_buy_amount());
assert!(exec.both_amounts_available());
}
#[test]
fn solver_execution_partial() {
let json = serde_json::json!({
"solver": "test_solver"
});
let exec: SolverExecution = serde_json::from_value(json).unwrap();
assert!(!exec.has_executed_sell_amount());
assert!(!exec.has_executed_buy_amount());
assert!(!exec.both_amounts_available());
}
#[test]
fn solver_execution_display() {
let json = serde_json::json!({"solver": "my_solver"});
let exec: SolverExecution = serde_json::from_value(json).unwrap();
assert_eq!(exec.to_string(), "exec(my_solver)");
}
#[test]
fn competition_order_status_no_value() {
let json = serde_json::json!({
"type": "open"
});
let status: CompetitionOrderStatus = serde_json::from_value(json).unwrap();
assert!(!status.has_value());
assert_eq!(status.value_len(), 0);
assert_eq!(status.to_string(), "open");
}
#[test]
fn competition_order_status_with_value() {
let json = serde_json::json!({
"type": "solved",
"value": [{"solver": "s1"}]
});
let status: CompetitionOrderStatus = serde_json::from_value(json).unwrap();
assert!(status.has_value());
assert_eq!(status.value_len(), 1);
assert_eq!(status.to_string(), "solved");
}
#[test]
fn total_surplus_new() {
let s = TotalSurplus::new("12345678");
assert_eq!(s.as_str(), "12345678");
}
#[test]
fn total_surplus_display() {
let s = TotalSurplus::new("42");
assert_eq!(s.to_string(), "surplus(42)");
}
#[test]
fn total_surplus_serde_roundtrip() {
let s = TotalSurplus::new("99999");
let json = serde_json::to_string(&s).unwrap();
let back: TotalSurplus = serde_json::from_str(&json).unwrap();
assert_eq!(back.as_str(), "99999");
}
#[test]
fn app_data_object_new() {
let obj = AppDataObject::new("{\"version\":\"1.0.0\"}");
assert_eq!(obj.as_str(), "{\"version\":\"1.0.0\"}");
assert!(!obj.is_empty());
assert_eq!(obj.len(), 19);
}
#[test]
fn app_data_object_empty() {
let obj = AppDataObject::new("");
assert!(obj.is_empty());
assert_eq!(obj.len(), 0);
}
#[test]
fn app_data_object_from_string() {
let obj: AppDataObject = "test".to_owned().into();
assert_eq!(obj.as_str(), "test");
}
#[test]
fn app_data_object_into_string() {
let obj = AppDataObject::new("hello");
let s: String = obj.into();
assert_eq!(s, "hello");
}
#[test]
fn app_data_object_display_short() {
let obj = AppDataObject::new("{}");
let s = obj.to_string();
assert!(s.contains("{}"));
}
#[test]
fn app_data_object_display_truncates_long() {
let long = "a]".repeat(20);
let obj = AppDataObject::new(long);
let s = obj.to_string();
assert!(s.starts_with("app-data("));
assert!(s.len() < 50);
}
#[test]
fn app_data_object_serde_roundtrip() {
let obj = AppDataObject::new("{\"version\":\"1.0.0\"}");
let json = serde_json::to_string(&obj).unwrap();
let back: AppDataObject = serde_json::from_str(&json).unwrap();
assert_eq!(back.as_str(), "{\"version\":\"1.0.0\"}");
}
#[test]
fn auction_empty() {
let auction = Auction { id: None, block: 100, orders: vec![], prices: HashMap::default() };
assert!(auction.is_empty());
assert_eq!(auction.len(), 0);
assert!(!auction.has_prices());
assert!(auction.get_price("0xtoken").is_none());
assert!(auction.order_at(0).is_none());
assert!(auction.find_order_by_uid("0xabc").is_none());
}
#[test]
fn auction_with_orders_and_prices() {
let order = minimal_order();
let uid = order.uid.clone();
let mut prices = HashMap::default();
prices.insert("0xtoken".to_owned(), "42".to_owned());
let auction = Auction { id: Some(7), block: 200, orders: vec![order], prices };
assert!(!auction.is_empty());
assert_eq!(auction.len(), 1);
assert!(auction.has_prices());
assert_eq!(auction.get_price("0xtoken"), Some("42"));
assert!(auction.order_at(0).is_some());
assert!(auction.order_at(1).is_none());
assert!(auction.find_order_by_uid(&uid).is_some());
assert!(auction.find_order_by_uid("nonexistent").is_none());
}
#[test]
fn auction_display() {
let auction =
Auction { id: Some(5), block: 300, orders: vec![], prices: HashMap::default() };
assert_eq!(auction.to_string(), "auction(5, 0 orders, block=300)");
}
#[test]
fn auction_display_no_id() {
let auction = Auction { id: None, block: 100, orders: vec![], prices: HashMap::default() };
assert_eq!(auction.to_string(), "auction(-1, 0 orders, block=100)");
}
#[test]
fn from_unsigned_order_uses_explicit_receiver_when_non_zero() {
use alloy_primitives::{Address, B256, U256, address};
use cow_signing::types::SigningResult;
use cow_types::UnsignedOrder;
let order = UnsignedOrder {
sell_token: address!("dac17f958d2ee523a2206206994597c13d831ec7"),
buy_token: address!("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"),
receiver: Address::ZERO,
sell_amount: U256::from(1_000_000u64),
buy_amount: U256::from(2_000u64),
valid_to: 1_700_000_000,
app_data: B256::from([0xab; 32]),
fee_amount: U256::from(42u64),
kind: OrderKind::Sell,
partially_fillable: false,
sell_token_balance: TokenBalance::Erc20,
buy_token_balance: TokenBalance::Erc20,
};
let from = address!("1111111111111111111111111111111111111111");
let receiver = address!("2222222222222222222222222222222222222222");
let signing = SigningResult {
signature: "0xdeadbeef".to_owned(),
signing_scheme: SigningScheme::Eip712,
};
let creation = OrderCreation::from_unsigned_order(&order, from, receiver, signing);
assert_eq!(creation.from, from);
assert_eq!(creation.receiver, receiver);
assert_eq!(creation.sell_amount, "1000000");
assert_eq!(creation.buy_amount, "2000");
assert_eq!(creation.fee_amount, "42");
assert_eq!(creation.signature, "0xdeadbeef");
assert!(creation.app_data.starts_with("0x"));
assert!(creation.quote_id.is_none());
}
#[test]
fn from_unsigned_order_falls_back_to_from_when_receiver_is_zero() {
use alloy_primitives::{Address, B256, U256, address};
use cow_signing::types::SigningResult;
use cow_types::UnsignedOrder;
let order = UnsignedOrder {
sell_token: Address::ZERO,
buy_token: Address::ZERO,
receiver: Address::ZERO,
sell_amount: U256::ZERO,
buy_amount: U256::ZERO,
valid_to: 0,
app_data: B256::ZERO,
fee_amount: U256::ZERO,
kind: OrderKind::Buy,
partially_fillable: true,
sell_token_balance: TokenBalance::Erc20,
buy_token_balance: TokenBalance::Erc20,
};
let from = address!("3333333333333333333333333333333333333333");
let signing =
SigningResult { signature: "0x".to_owned(), signing_scheme: SigningScheme::EthSign };
let creation = OrderCreation::from_unsigned_order(&order, from, Address::ZERO, signing);
assert_eq!(creation.receiver, from);
assert_eq!(creation.kind, OrderKind::Buy);
assert!(creation.partially_fillable);
}
#[test]
fn order_class_display_matches_as_str() {
for class in [OrderClass::Market, OrderClass::Limit, OrderClass::Liquidity] {
assert_eq!(class.to_string(), class.as_str());
}
}
}