use {
crate::{
domain::DomainSeparator,
order::OrderUid,
signature::{
EcdsaSignature, SignatureError, ecdsa_recover, ecdsa_wire, sign_ecdsa, sign_ecdsa_async,
},
signing_scheme::EcdsaSigningScheme,
},
alloy_primitives::{Address, B256},
serde::{Deserialize, Serialize},
};
mod eip712 {
use {super::OrderUid, alloy_primitives::Bytes, alloy_sol_types::sol};
sol! {
struct OrderCancellation {
bytes orderUid;
}
struct OrderCancellations {
bytes[] orderUids;
}
}
pub(super) fn single(uid: &OrderUid) -> OrderCancellation {
OrderCancellation {
orderUid: Bytes::from(uid.0),
}
}
pub(super) fn collection(uids: &[OrderUid]) -> OrderCancellations {
OrderCancellations {
orderUids: uids.iter().map(|u| Bytes::from(u.0)).collect(),
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderCancellation {
pub order_uid: OrderUid,
}
impl From<OrderUid> for OrderCancellation {
fn from(order_uid: OrderUid) -> Self {
Self { order_uid }
}
}
impl OrderCancellation {
pub fn hash_struct(&self) -> B256 {
use alloy_sol_types::SolStruct;
eip712::single(&self.order_uid).eip712_hash_struct()
}
pub fn signing_hash(&self, scheme: EcdsaSigningScheme, domain: &DomainSeparator) -> B256 {
crate::signature::signing_message(scheme, domain, &eip712::single(&self.order_uid))
}
pub fn sign<S: alloy_signer::SignerSync>(
self,
scheme: EcdsaSigningScheme,
domain: &DomainSeparator,
signer: &S,
) -> Result<SignedOrderCancellation, SignatureError> {
let signature = sign_ecdsa(scheme, domain, &eip712::single(&self.order_uid), signer)?;
Ok(SignedOrderCancellation {
order_uid: self.order_uid,
signature,
signing_scheme: scheme,
})
}
pub async fn sign_async<S: alloy_signer::Signer>(
self,
scheme: EcdsaSigningScheme,
domain: &DomainSeparator,
signer: &S,
) -> Result<SignedOrderCancellation, SignatureError> {
let signature =
sign_ecdsa_async(scheme, domain, &eip712::single(&self.order_uid), signer).await?;
Ok(SignedOrderCancellation {
order_uid: self.order_uid,
signature,
signing_scheme: scheme,
})
}
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignedOrderCancellation {
pub order_uid: OrderUid,
#[serde(with = "ecdsa_wire")]
pub signature: EcdsaSignature,
pub signing_scheme: EcdsaSigningScheme,
}
impl SignedOrderCancellation {
#[deprecated(
since = "0.2.0",
note = "use OrderCancellation::from(uid).hash_struct() instead"
)]
pub fn hash_struct(uid: &OrderUid) -> B256 {
OrderCancellation::from(*uid).hash_struct()
}
#[deprecated(
since = "0.2.0",
note = "use OrderCancellation::from(uid).sign(scheme, domain, signer) instead"
)]
pub fn sign<S: alloy_signer::SignerSync>(
order_uid: OrderUid,
scheme: EcdsaSigningScheme,
domain: &DomainSeparator,
signer: &S,
) -> Result<Self, SignatureError> {
OrderCancellation::from(order_uid).sign(scheme, domain, signer)
}
pub fn recover_owner(&self, domain: &DomainSeparator) -> Result<Address, SignatureError> {
let payload = eip712::single(&self.order_uid);
Ok(ecdsa_recover(&self.signature, self.signing_scheme, domain, &payload)?.signer)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderCancellations {
pub order_uids: Vec<OrderUid>,
}
impl From<Vec<OrderUid>> for OrderCancellations {
fn from(order_uids: Vec<OrderUid>) -> Self {
Self { order_uids }
}
}
impl FromIterator<OrderUid> for OrderCancellations {
fn from_iter<I: IntoIterator<Item = OrderUid>>(iter: I) -> Self {
Self {
order_uids: iter.into_iter().collect(),
}
}
}
impl IntoIterator for OrderCancellations {
type Item = OrderUid;
type IntoIter = std::vec::IntoIter<OrderUid>;
fn into_iter(self) -> Self::IntoIter {
self.order_uids.into_iter()
}
}
impl OrderCancellations {
pub fn hash_struct(&self) -> B256 {
use alloy_sol_types::SolStruct;
eip712::collection(&self.order_uids).eip712_hash_struct()
}
pub fn sign<S: alloy_signer::SignerSync>(
self,
scheme: EcdsaSigningScheme,
domain: &DomainSeparator,
signer: &S,
) -> Result<SignedOrderCancellations, SignatureError> {
let payload = eip712::collection(&self.order_uids);
let signature = sign_ecdsa(scheme, domain, &payload, signer)?;
Ok(SignedOrderCancellations {
order_uids: self.order_uids,
signature,
signing_scheme: scheme,
})
}
pub async fn sign_async<S: alloy_signer::Signer>(
self,
scheme: EcdsaSigningScheme,
domain: &DomainSeparator,
signer: &S,
) -> Result<SignedOrderCancellations, SignatureError> {
let payload = eip712::collection(&self.order_uids);
let signature = sign_ecdsa_async(scheme, domain, &payload, signer).await?;
Ok(SignedOrderCancellations {
order_uids: self.order_uids,
signature,
signing_scheme: scheme,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SignedOrderCancellations {
pub order_uids: Vec<OrderUid>,
#[serde(with = "ecdsa_wire")]
pub signature: EcdsaSignature,
pub signing_scheme: EcdsaSigningScheme,
}
impl SignedOrderCancellations {
pub fn recover_owner(&self, domain: &DomainSeparator) -> Result<Address, SignatureError> {
let payload = eip712::collection(&self.order_uids);
Ok(ecdsa_recover(&self.signature, self.signing_scheme, domain, &payload)?.signer)
}
}
fn cancellation_type_entries() -> Vec<serde_json::Value> {
use alloy_sol_types::SolStruct;
let root = <eip712::OrderCancellation as SolStruct>::eip712_root_type();
let fields = root
.strip_prefix("OrderCancellation(")
.and_then(|s| s.strip_suffix(')'))
.expect("canonical OrderCancellation root type is `OrderCancellation(...)`");
fields
.split(',')
.map(|field| {
let (sol_type, name) = field
.split_once(' ')
.expect("each EIP-712 field is `<type> <name>`");
serde_json::json!({ "name": name, "type": sol_type })
})
.collect()
}
pub fn cancellation_typed_data(
cancellation: &OrderCancellation,
chain_id: u64,
verifying_contract: Address,
) -> serde_json::Value {
let message = serde_json::to_value(cancellation).expect("OrderCancellation serialises to JSON");
serde_json::json!({
"domain": {
"name": crate::domain::DOMAIN_NAME,
"version": crate::domain::DOMAIN_VERSION,
"chainId": chain_id,
"verifyingContract": verifying_contract.to_string(),
},
"primaryType": "OrderCancellation",
"types": { "OrderCancellation": cancellation_type_entries() },
"message": message,
})
}
#[cfg(test)]
mod tests {
use {
super::*,
alloy_primitives::{Bytes, U256, b256, keccak256},
alloy_signer_local::PrivateKeySigner,
};
#[test]
fn order_cancellation_type_hash_matches_canonical_signature() {
use alloy_sol_types::SolStruct;
let signature = b"OrderCancellation(bytes orderUid)";
let sol = eip712::OrderCancellation {
orderUid: Bytes::copy_from_slice(&[0u8; 56]),
};
assert_eq!(
<eip712::OrderCancellation as SolStruct>::eip712_type_hash(&sol),
keccak256(signature),
);
}
#[test]
fn order_cancellations_type_hash_matches_canonical_signature() {
use alloy_sol_types::SolStruct;
let signature = b"OrderCancellations(bytes[] orderUids)";
let sol = eip712::OrderCancellations { orderUids: vec![] };
assert_eq!(
<eip712::OrderCancellations as SolStruct>::eip712_type_hash(&sol),
keccak256(signature),
);
}
#[test]
fn order_cancellations_hash_struct_matches_services_golden() {
let empty = OrderCancellations::default();
assert_eq!(
empty.hash_struct(),
b256!("56acdb3034898c6c23971cb3f92c32a4739e89a13c85282547025583a93911bd")
);
let two = OrderCancellations {
order_uids: vec![OrderUid::from([0x11; 56]), OrderUid::from([0x22; 56])],
};
assert_eq!(
two.hash_struct(),
b256!("405f6cb53d87901a5385a824a99c94b43146547f5ea3623f8d2f50b925e97a8b")
);
}
#[test]
fn order_cancellation_hash_struct_matches_independent_eip712_derivation() {
let type_hash = keccak256(b"OrderCancellation(bytes orderUid)");
for uid in [OrderUid::from([0u8; 56]), OrderUid::from([0x42; 56])] {
let mut encoded = [0u8; 64];
encoded[0..32].copy_from_slice(type_hash.as_slice());
encoded[32..64].copy_from_slice(keccak256(uid.as_slice()).as_slice());
let expected = keccak256(encoded);
assert_eq!(OrderCancellation::from(uid).hash_struct(), expected);
}
}
fn fixed_signer() -> PrivateKeySigner {
PrivateKeySigner::from_bytes(&U256::from(1u64).to_be_bytes().into()).unwrap()
}
fn fixed_domain() -> DomainSeparator {
crate::domain::settlement_domain(
1,
alloy_primitives::address!("9008D19f58AAbD9eD0D60971565AA8510560ab41"),
)
}
#[test]
fn order_cancellation_sign_recover_round_trip() {
let signer = fixed_signer();
let domain = fixed_domain();
let uid = OrderUid::from([0x42; 56]);
for scheme in [EcdsaSigningScheme::Eip712, EcdsaSigningScheme::EthSign] {
let cancellation = OrderCancellation::from(uid)
.sign(scheme, &domain, &signer)
.unwrap();
let recovered = cancellation.recover_owner(&domain).unwrap();
assert_eq!(recovered, signer.address());
}
}
#[test]
fn order_cancellations_sign_recover_round_trip() {
let signer = fixed_signer();
let domain = fixed_domain();
let cancellations = OrderCancellations {
order_uids: vec![OrderUid::from([0x11; 56]), OrderUid::from([0x22; 56])],
};
let signed = cancellations
.sign(EcdsaSigningScheme::Eip712, &domain, &signer)
.unwrap();
let recovered = signed.recover_owner(&domain).unwrap();
assert_eq!(recovered, signer.address());
}
#[tokio::test]
async fn cancellation_sign_async_matches_sync() {
let signer = fixed_signer();
let domain = fixed_domain();
let uid = OrderUid::from([0x42; 56]);
for scheme in [EcdsaSigningScheme::Eip712, EcdsaSigningScheme::EthSign] {
let sync = OrderCancellation::from(uid)
.sign(scheme, &domain, &signer)
.unwrap();
let asynchronous = OrderCancellation::from(uid)
.sign_async(scheme, &domain, &signer)
.await
.unwrap();
assert_eq!(sync, asynchronous);
assert_eq!(
asynchronous.recover_owner(&domain).unwrap(),
signer.address()
);
}
let collection = OrderCancellations {
order_uids: vec![OrderUid::from([0x11; 56]), OrderUid::from([0x22; 56])],
};
let sync = collection
.clone()
.sign(EcdsaSigningScheme::Eip712, &domain, &signer)
.unwrap();
let asynchronous = collection
.sign_async(EcdsaSigningScheme::Eip712, &domain, &signer)
.await
.unwrap();
assert_eq!(sync, asynchronous);
assert_eq!(
asynchronous.recover_owner(&domain).unwrap(),
signer.address()
);
}
#[test]
fn signed_cancellations_wire_format() {
let signed = SignedOrderCancellations {
order_uids: vec![OrderUid::from([0x11; 56])],
signature: EcdsaSignature::from_bytes_and_parity(&[0u8; 64], false),
signing_scheme: EcdsaSigningScheme::Eip712,
};
let body = serde_json::to_value(&signed).unwrap();
assert!(body["orderUids"].is_array());
assert_eq!(body["signingScheme"], "eip712");
assert!(body["signature"].as_str().unwrap().starts_with("0x"));
}
#[test]
fn order_cancellation_json_round_trip() {
let original = OrderCancellation::from(OrderUid::from([0x77; 56]))
.sign(EcdsaSigningScheme::Eip712, &fixed_domain(), &fixed_signer())
.unwrap();
let json = serde_json::to_string(&original).unwrap();
let parsed: SignedOrderCancellation = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, original);
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(value.get("orderUid").is_some());
assert!(value.get("signingScheme").is_some());
}
#[test]
fn order_cancellations_json_round_trip() {
let original = OrderCancellations {
order_uids: vec![OrderUid::from([0x01; 56]), OrderUid::from([0x02; 56])],
};
let json = serde_json::to_string(&original).unwrap();
let parsed: OrderCancellations = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, original);
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(value.get("orderUids").is_some());
}
#[test]
fn signed_order_cancellations_json_round_trip() {
let original = OrderCancellations {
order_uids: vec![OrderUid::from([0x33; 56]), OrderUid::from([0x44; 56])],
}
.sign(
EcdsaSigningScheme::EthSign,
&fixed_domain(),
&fixed_signer(),
)
.unwrap();
let json = serde_json::to_string(&original).unwrap();
let parsed: SignedOrderCancellations = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, original);
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(value.get("orderUids").is_some());
assert!(value.get("signature").is_some());
assert!(value.get("signingScheme").is_some());
}
#[test]
#[allow(deprecated)]
fn deprecated_single_cancel_delegates_to_new_type() {
let signer = fixed_signer();
let domain = fixed_domain();
let uid = OrderUid::from([0x42; 56]);
assert_eq!(
SignedOrderCancellation::hash_struct(&uid),
OrderCancellation::from(uid).hash_struct(),
);
for scheme in [EcdsaSigningScheme::Eip712, EcdsaSigningScheme::EthSign] {
let old = SignedOrderCancellation::sign(uid, scheme, &domain, &signer).unwrap();
let new = OrderCancellation::from(uid)
.sign(scheme, &domain, &signer)
.unwrap();
assert_eq!(old, new);
}
}
#[test]
fn cancellation_signing_hash_matches_recovered_message() {
let signer = fixed_signer();
let domain = fixed_domain();
let uid = OrderUid::from([0x42; 56]);
for scheme in [EcdsaSigningScheme::Eip712, EcdsaSigningScheme::EthSign] {
let cancellation = OrderCancellation::from(uid);
let signed = cancellation.sign(scheme, &domain, &signer).unwrap();
let recovered =
ecdsa_recover(&signed.signature, scheme, &domain, &eip712::single(&uid)).unwrap();
assert_eq!(
recovered.message,
cancellation.signing_hash(scheme, &domain)
);
}
}
#[test]
fn cancellation_typed_data_table_matches_canonical_field() {
let typed = cancellation_typed_data(
&OrderCancellation::from(OrderUid::from([0x11; 56])),
1,
Address::ZERO,
);
let table = typed["types"]["OrderCancellation"].as_array().unwrap();
assert_eq!(table.len(), 1);
assert_eq!(table[0]["name"], "orderUid");
assert_eq!(table[0]["type"], "bytes");
}
#[test]
fn cancellation_typed_data_envelope_shape() {
let uid = OrderUid::from([0x11; 56]);
let contract = alloy_primitives::address!("9008D19f58AAbD9eD0D60971565AA8510560ab41");
let typed = cancellation_typed_data(&OrderCancellation::from(uid), 1, contract);
assert_eq!(typed["domain"]["name"], crate::domain::DOMAIN_NAME);
assert_eq!(typed["domain"]["version"], crate::domain::DOMAIN_VERSION);
assert_eq!(typed["domain"]["chainId"], 1);
assert_eq!(typed["domain"]["verifyingContract"], contract.to_string());
assert_eq!(typed["primaryType"], "OrderCancellation");
assert!(typed["types"].get("EIP712Domain").is_none());
assert_eq!(typed["message"]["orderUid"], uid.to_string());
}
}