#![deny(unsafe_code)]
#![warn(missing_docs)]
pub mod flags;
pub mod trade;
mod unsigned_order;
pub use unsigned_order::UnsignedOrder;
use std::fmt;
use cow_errors::CowError;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CowHook {
pub target: String,
pub call_data: String,
pub gas_limit: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub dapp_id: Option<String>,
}
impl CowHook {
#[must_use]
pub fn new(
target: impl Into<String>,
call_data: impl Into<String>,
gas_limit: impl Into<String>,
) -> Self {
Self {
target: target.into(),
call_data: call_data.into(),
gas_limit: gas_limit.into(),
dapp_id: None,
}
}
#[must_use]
pub fn with_dapp_id(mut self, dapp_id: impl Into<String>) -> Self {
self.dapp_id = Some(dapp_id.into());
self
}
#[must_use]
pub const fn has_dapp_id(&self) -> bool {
self.dapp_id.is_some()
}
}
impl fmt::Display for CowHook {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "hook(target={}, gas={})", self.target, self.gas_limit)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnchainOrderData {
pub sender: alloy_primitives::Address,
pub placement_error: Option<String>,
}
impl OnchainOrderData {
#[must_use]
pub const fn new(sender: alloy_primitives::Address) -> Self {
Self { sender, placement_error: None }
}
#[must_use]
pub const fn has_placement_error(&self) -> bool {
self.placement_error.is_some()
}
}
impl fmt::Display for OnchainOrderData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "onchain(sender={:#x})", self.sender)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OrderKind {
Sell,
Buy,
}
impl OrderKind {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Sell => "sell",
Self::Buy => "buy",
}
}
#[must_use]
pub const fn is_sell(self) -> bool {
matches!(self, Self::Sell)
}
#[must_use]
pub const fn is_buy(self) -> bool {
matches!(self, Self::Buy)
}
}
impl fmt::Display for OrderKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum TokenBalance {
#[default]
Erc20,
External,
Internal,
}
impl TokenBalance {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Erc20 => "erc20",
Self::External => "external",
Self::Internal => "internal",
}
}
#[must_use]
pub fn eip712_hash(self) -> alloy_primitives::B256 {
alloy_primitives::keccak256(self.as_str().as_bytes())
}
#[must_use]
pub const fn is_erc20(self) -> bool {
matches!(self, Self::Erc20)
}
#[must_use]
pub const fn is_external(self) -> bool {
matches!(self, Self::External)
}
#[must_use]
pub const fn is_internal(self) -> bool {
matches!(self, Self::Internal)
}
}
impl fmt::Display for TokenBalance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SigningScheme {
Eip712,
EthSign,
Eip1271,
PreSign,
}
impl SigningScheme {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Eip712 => "eip712",
Self::EthSign => "ethsign",
Self::Eip1271 => "eip1271",
Self::PreSign => "presign",
}
}
#[must_use]
pub const fn is_eip712(self) -> bool {
matches!(self, Self::Eip712)
}
#[must_use]
pub const fn is_eth_sign(self) -> bool {
matches!(self, Self::EthSign)
}
#[must_use]
pub const fn is_eip1271(self) -> bool {
matches!(self, Self::Eip1271)
}
#[must_use]
pub const fn is_presign(self) -> bool {
matches!(self, Self::PreSign)
}
}
impl fmt::Display for SigningScheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum EcdsaSigningScheme {
#[default]
Eip712,
EthSign,
}
impl EcdsaSigningScheme {
#[must_use]
pub const fn into_signing_scheme(self) -> SigningScheme {
match self {
Self::Eip712 => SigningScheme::Eip712,
Self::EthSign => SigningScheme::EthSign,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Eip712 => "eip712",
Self::EthSign => "ethsign",
}
}
#[must_use]
pub const fn is_eip712(self) -> bool {
matches!(self, Self::Eip712)
}
#[must_use]
pub const fn is_eth_sign(self) -> bool {
matches!(self, Self::EthSign)
}
}
impl fmt::Display for EcdsaSigningScheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl From<EcdsaSigningScheme> for SigningScheme {
fn from(s: EcdsaSigningScheme) -> Self {
s.into_signing_scheme()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum PriceQuality {
Fast,
#[default]
Optimal,
Verified,
}
impl PriceQuality {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Fast => "fast",
Self::Optimal => "optimal",
Self::Verified => "verified",
}
}
#[must_use]
pub const fn is_fast(self) -> bool {
matches!(self, Self::Fast)
}
#[must_use]
pub const fn is_optimal(self) -> bool {
matches!(self, Self::Optimal)
}
#[must_use]
pub const fn is_verified(self) -> bool {
matches!(self, Self::Verified)
}
}
impl fmt::Display for PriceQuality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl TryFrom<&str> for OrderKind {
type Error = CowError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"sell" => Ok(Self::Sell),
"buy" => Ok(Self::Buy),
other => Err(CowError::Parse {
field: "OrderKind",
reason: format!("unknown value: {other}"),
}),
}
}
}
impl TryFrom<&str> for TokenBalance {
type Error = CowError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"erc20" => Ok(Self::Erc20),
"external" => Ok(Self::External),
"internal" => Ok(Self::Internal),
other => Err(CowError::Parse {
field: "TokenBalance",
reason: format!("unknown value: {other}"),
}),
}
}
}
impl TryFrom<&str> for SigningScheme {
type Error = CowError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"eip712" => Ok(Self::Eip712),
"ethsign" => Ok(Self::EthSign),
"eip1271" => Ok(Self::Eip1271),
"presign" => Ok(Self::PreSign),
other => Err(CowError::Parse {
field: "SigningScheme",
reason: format!("unknown value: {other}"),
}),
}
}
}
impl TryFrom<&str> for EcdsaSigningScheme {
type Error = CowError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"eip712" => Ok(Self::Eip712),
"ethsign" => Ok(Self::EthSign),
other => Err(CowError::Parse {
field: "EcdsaSigningScheme",
reason: format!("unknown value: {other}"),
}),
}
}
}
impl TryFrom<&str> for PriceQuality {
type Error = CowError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
match s {
"fast" => Ok(Self::Fast),
"optimal" => Ok(Self::Optimal),
"verified" => Ok(Self::Verified),
other => Err(CowError::Parse {
field: "PriceQuality",
reason: format!("unknown value: {other}"),
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn order_kind_as_str() {
assert_eq!(OrderKind::Sell.as_str(), "sell");
assert_eq!(OrderKind::Buy.as_str(), "buy");
}
#[test]
fn order_kind_predicates() {
assert!(OrderKind::Sell.is_sell());
assert!(!OrderKind::Sell.is_buy());
assert!(OrderKind::Buy.is_buy());
assert!(!OrderKind::Buy.is_sell());
}
#[test]
fn order_kind_display() {
assert_eq!(format!("{}", OrderKind::Sell), "sell");
assert_eq!(format!("{}", OrderKind::Buy), "buy");
}
#[test]
fn order_kind_roundtrip() {
for kind in [OrderKind::Sell, OrderKind::Buy] {
let parsed = OrderKind::try_from(kind.as_str()).unwrap();
assert_eq!(parsed, kind);
}
}
#[test]
fn order_kind_invalid() {
assert!(OrderKind::try_from("invalid").is_err());
assert!(OrderKind::try_from("").is_err());
assert!(OrderKind::try_from("SELL").is_err());
}
#[test]
fn order_kind_serde_roundtrip() {
let json = serde_json::to_string(&OrderKind::Sell).unwrap();
assert_eq!(json, "\"sell\"");
let back: OrderKind = serde_json::from_str(&json).unwrap();
assert_eq!(back, OrderKind::Sell);
}
#[test]
fn token_balance_as_str() {
assert_eq!(TokenBalance::Erc20.as_str(), "erc20");
assert_eq!(TokenBalance::External.as_str(), "external");
assert_eq!(TokenBalance::Internal.as_str(), "internal");
}
#[test]
fn token_balance_predicates() {
assert!(TokenBalance::Erc20.is_erc20());
assert!(!TokenBalance::Erc20.is_external());
assert!(!TokenBalance::Erc20.is_internal());
assert!(TokenBalance::External.is_external());
assert!(TokenBalance::Internal.is_internal());
}
#[test]
fn token_balance_default() {
assert_eq!(TokenBalance::default(), TokenBalance::Erc20);
}
#[test]
fn token_balance_roundtrip() {
for bal in [TokenBalance::Erc20, TokenBalance::External, TokenBalance::Internal] {
let parsed = TokenBalance::try_from(bal.as_str()).unwrap();
assert_eq!(parsed, bal);
}
}
#[test]
fn token_balance_invalid() {
assert!(TokenBalance::try_from("ERC20").is_err());
assert!(TokenBalance::try_from("").is_err());
}
#[test]
fn token_balance_eip712_hash_deterministic() {
let h1 = TokenBalance::Erc20.eip712_hash();
let h2 = TokenBalance::Erc20.eip712_hash();
assert_eq!(h1, h2);
assert_ne!(TokenBalance::Erc20.eip712_hash(), TokenBalance::External.eip712_hash());
assert_ne!(TokenBalance::External.eip712_hash(), TokenBalance::Internal.eip712_hash());
}
#[test]
fn token_balance_display() {
assert_eq!(format!("{}", TokenBalance::External), "external");
}
#[test]
fn signing_scheme_as_str() {
assert_eq!(SigningScheme::Eip712.as_str(), "eip712");
assert_eq!(SigningScheme::EthSign.as_str(), "ethsign");
assert_eq!(SigningScheme::Eip1271.as_str(), "eip1271");
assert_eq!(SigningScheme::PreSign.as_str(), "presign");
}
#[test]
fn signing_scheme_predicates() {
assert!(SigningScheme::Eip712.is_eip712());
assert!(SigningScheme::EthSign.is_eth_sign());
assert!(SigningScheme::Eip1271.is_eip1271());
assert!(SigningScheme::PreSign.is_presign());
assert!(!SigningScheme::Eip712.is_presign());
}
#[test]
fn signing_scheme_roundtrip() {
for s in [
SigningScheme::Eip712,
SigningScheme::EthSign,
SigningScheme::Eip1271,
SigningScheme::PreSign,
] {
assert_eq!(SigningScheme::try_from(s.as_str()).unwrap(), s);
}
}
#[test]
fn signing_scheme_invalid() {
assert!(SigningScheme::try_from("eip-712").is_err());
assert!(SigningScheme::try_from("").is_err());
}
#[test]
fn signing_scheme_display() {
assert_eq!(format!("{}", SigningScheme::PreSign), "presign");
}
#[test]
fn ecdsa_scheme_default() {
assert_eq!(EcdsaSigningScheme::default(), EcdsaSigningScheme::Eip712);
}
#[test]
fn ecdsa_scheme_into_signing_scheme() {
assert_eq!(EcdsaSigningScheme::Eip712.into_signing_scheme(), SigningScheme::Eip712);
assert_eq!(EcdsaSigningScheme::EthSign.into_signing_scheme(), SigningScheme::EthSign);
}
#[test]
fn ecdsa_scheme_from_conversion() {
let full: SigningScheme = EcdsaSigningScheme::EthSign.into();
assert_eq!(full, SigningScheme::EthSign);
}
#[test]
fn ecdsa_scheme_predicates() {
assert!(EcdsaSigningScheme::Eip712.is_eip712());
assert!(!EcdsaSigningScheme::Eip712.is_eth_sign());
assert!(EcdsaSigningScheme::EthSign.is_eth_sign());
}
#[test]
fn ecdsa_scheme_roundtrip() {
for s in [EcdsaSigningScheme::Eip712, EcdsaSigningScheme::EthSign] {
assert_eq!(EcdsaSigningScheme::try_from(s.as_str()).unwrap(), s);
}
}
#[test]
fn ecdsa_scheme_invalid() {
assert!(EcdsaSigningScheme::try_from("eip1271").is_err());
}
#[test]
fn ecdsa_scheme_display() {
assert_eq!(format!("{}", EcdsaSigningScheme::EthSign), "ethsign");
}
#[test]
fn price_quality_default() {
assert_eq!(PriceQuality::default(), PriceQuality::Optimal);
}
#[test]
fn price_quality_as_str() {
assert_eq!(PriceQuality::Fast.as_str(), "fast");
assert_eq!(PriceQuality::Optimal.as_str(), "optimal");
assert_eq!(PriceQuality::Verified.as_str(), "verified");
}
#[test]
fn price_quality_predicates() {
assert!(PriceQuality::Fast.is_fast());
assert!(PriceQuality::Optimal.is_optimal());
assert!(PriceQuality::Verified.is_verified());
assert!(!PriceQuality::Fast.is_optimal());
}
#[test]
fn price_quality_roundtrip() {
for q in [PriceQuality::Fast, PriceQuality::Optimal, PriceQuality::Verified] {
assert_eq!(PriceQuality::try_from(q.as_str()).unwrap(), q);
}
}
#[test]
fn price_quality_invalid() {
assert!(PriceQuality::try_from("slow").is_err());
}
#[test]
fn price_quality_display() {
assert_eq!(format!("{}", PriceQuality::Verified), "verified");
}
#[test]
fn cow_hook_with_dapp_id_sets_field() {
let hook = CowHook::new("0xtarget", "0xdata", "21000").with_dapp_id("my-dapp");
assert!(hook.has_dapp_id());
assert_eq!(hook.dapp_id.as_deref(), Some("my-dapp"));
}
#[test]
fn cow_hook_has_dapp_id_false_by_default() {
let hook = CowHook::new("0xtarget", "0xdata", "21000");
assert!(!hook.has_dapp_id());
}
#[test]
fn cow_hook_display_renders_target_and_gas() {
let hook = CowHook::new("0xabc", "0xff", "50000");
assert_eq!(format!("{hook}"), "hook(target=0xabc, gas=50000)");
}
#[test]
fn onchain_order_data_has_placement_error_predicates() {
let mut data = OnchainOrderData::new(alloy_primitives::Address::ZERO);
assert!(!data.has_placement_error());
data.placement_error = Some("rejected".into());
assert!(data.has_placement_error());
}
#[test]
fn onchain_order_data_display_includes_sender() {
let data = OnchainOrderData::new(alloy_primitives::Address::ZERO);
let rendered = format!("{data}");
assert!(rendered.starts_with("onchain(sender=0x"));
}
}