use alloy_primitives::{Address, B256, U256, keccak256};
use hex_literal::hex;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use serde_with::{DisplayFromStr, serde_as};
use std::fmt::{self, Debug, Display};
use std::str::FromStr;
use crate::app_data::AppDataHash;
use crate::domain::{DomainSeparator, hashed_eip712_message};
pub const BUY_ETH_ADDRESS: Address = Address::repeat_byte(0xee);
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum OrderStatus {
PresignaturePending,
#[default]
Open,
Fulfilled,
Cancelled,
Expired,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OrderClass {
#[default]
Market,
Liquidity,
Limit,
}
#[serde_as]
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderData {
pub sell_token: Address,
pub buy_token: Address,
#[serde(default)]
pub receiver: Option<Address>,
#[serde_as(as = "DisplayFromStr")]
pub sell_amount: U256,
#[serde_as(as = "DisplayFromStr")]
pub buy_amount: U256,
pub valid_to: u32,
pub app_data: AppDataHash,
#[serde_as(as = "DisplayFromStr")]
pub fee_amount: U256,
pub kind: OrderKind,
pub partially_fillable: bool,
#[serde(default)]
pub sell_token_balance: SellTokenSource,
#[serde(default)]
pub buy_token_balance: BuyTokenDestination,
}
impl OrderData {
pub const TYPE_HASH: [u8; 32] =
hex!("d5a25ba2e97094ad7d83dc28a6572da797d6b3e7fc6663bd93efb789fc17e489");
pub fn hash_struct(&self) -> [u8; 32] {
let mut hash_data = [0u8; 416];
hash_data[0..32].copy_from_slice(&Self::TYPE_HASH);
hash_data[44..64].copy_from_slice(self.sell_token.as_slice());
hash_data[76..96].copy_from_slice(self.buy_token.as_slice());
hash_data[108..128].copy_from_slice(self.receiver.unwrap_or(Address::ZERO).as_slice());
hash_data[128..160].copy_from_slice(&self.sell_amount.to_be_bytes::<32>());
hash_data[160..192].copy_from_slice(&self.buy_amount.to_be_bytes::<32>());
hash_data[220..224].copy_from_slice(&self.valid_to.to_be_bytes());
hash_data[224..256].copy_from_slice(&self.app_data.0);
hash_data[256..288].copy_from_slice(&self.fee_amount.to_be_bytes::<32>());
hash_data[288..320].copy_from_slice(match self.kind {
OrderKind::Sell => &OrderKind::SELL,
OrderKind::Buy => &OrderKind::BUY,
});
hash_data[351] = self.partially_fillable as u8;
hash_data[352..384].copy_from_slice(&self.sell_token_balance.as_bytes());
hash_data[384..416].copy_from_slice(&self.buy_token_balance.as_bytes());
*keccak256(hash_data)
}
pub fn uid(&self, domain: &DomainSeparator, owner: Address) -> OrderUid {
OrderUid::from_parts(
hashed_eip712_message(domain, &self.hash_struct()),
owner,
self.valid_to,
)
}
pub fn sign<S: alloy_signer::SignerSync>(
&self,
scheme: crate::signing_scheme::EcdsaSigningScheme,
domain: &DomainSeparator,
signer: &S,
) -> Result<crate::signature::Signature, crate::signature::SignatureError> {
let ecdsa =
crate::signature::EcdsaSignature::sign(scheme, domain, &self.hash_struct(), signer)?;
Ok(ecdsa.to_signature(scheme))
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct OrderBuilder(OrderData);
impl OrderBuilder {
pub fn new(sell_token: Address, buy_token: Address) -> Self {
Self(OrderData {
sell_token,
buy_token,
..OrderData::default()
})
}
pub const fn from_order_data(data: OrderData) -> Self {
Self(data)
}
pub fn receiver(mut self, receiver: Option<Address>) -> Self {
self.0.receiver = match receiver {
Some(addr) if addr == Address::ZERO => None,
other => other,
};
self
}
pub const fn sell_amount(mut self, amount: U256) -> Self {
self.0.sell_amount = amount;
self
}
pub const fn buy_amount(mut self, amount: U256) -> Self {
self.0.buy_amount = amount;
self
}
pub const fn valid_to(mut self, valid_to: u32) -> Self {
self.0.valid_to = valid_to;
self
}
pub const fn app_data(mut self, hash: AppDataHash) -> Self {
self.0.app_data = hash;
self
}
pub const fn fee_amount(mut self, amount: U256) -> Self {
self.0.fee_amount = amount;
self
}
pub const fn kind(mut self, kind: OrderKind) -> Self {
self.0.kind = kind;
self
}
pub const fn partially_fillable(mut self, partially_fillable: bool) -> Self {
self.0.partially_fillable = partially_fillable;
self
}
pub const fn sell_token_balance(mut self, balance: SellTokenSource) -> Self {
self.0.sell_token_balance = balance;
self
}
pub const fn buy_token_balance(mut self, balance: BuyTokenDestination) -> Self {
self.0.buy_token_balance = balance;
self
}
pub const fn build(self) -> OrderData {
self.0
}
pub fn sign<S: alloy_signer::SignerSync>(
self,
scheme: crate::signing_scheme::EcdsaSigningScheme,
domain: &DomainSeparator,
signer: &S,
) -> Result<crate::signature::Signature, crate::signature::SignatureError> {
self.0.sign(scheme, domain, signer)
}
}
#[serde_as]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Order {
#[serde(flatten)]
pub data: OrderData,
pub uid: OrderUid,
pub owner: alloy_primitives::Address,
pub signing_scheme: crate::signing_scheme::SigningScheme,
pub signature: String,
pub creation_date: String,
pub status: OrderStatus,
pub class: OrderClass,
#[serde_as(as = "DisplayFromStr")]
pub executed_buy_amount: alloy_primitives::U256,
#[serde_as(as = "DisplayFromStr")]
pub executed_sell_amount: alloy_primitives::U256,
#[serde_as(as = "Option<DisplayFromStr>")]
#[serde(default)]
pub executed_fee: Option<alloy_primitives::U256>,
#[serde(default)]
pub executed_fee_token: Option<alloy_primitives::Address>,
#[serde(default)]
pub invalidated: bool,
#[serde(default)]
pub is_liquidity_order: bool,
#[serde(default)]
pub full_app_data: Option<String>,
#[serde(default)]
pub quote: Option<serde_json::Value>,
#[serde(default)]
pub interactions: Option<serde_json::Value>,
#[serde(default)]
pub ethflow_data: Option<serde_json::Value>,
#[serde(default)]
pub onchain_order_data: Option<serde_json::Value>,
#[serde(default)]
pub onchain_user: Option<alloy_primitives::Address>,
#[serde(default)]
pub settlement_contract: Option<alloy_primitives::Address>,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum OrderKind {
#[default]
Buy,
Sell,
}
impl OrderKind {
pub const BUY: [u8; 32] =
hex!("6ed88e868af0a1983e3886d5f3e95a2fafbd6c3450bc229e27342283dc429ccc");
pub const SELL: [u8; 32] =
hex!("f3b277728b3fee749481eb3e0b3b48980dbbab78658fc419025cb16eee346775");
pub const fn as_str(self) -> &'static str {
match self {
Self::Buy => "buy",
Self::Sell => "sell",
}
}
pub const fn as_bytes(self) -> [u8; 32] {
match self {
Self::Buy => Self::BUY,
Self::Sell => Self::SELL,
}
}
pub const fn from_contract_bytes(bytes: [u8; 32]) -> Option<Self> {
if matches_bytes(&bytes, &Self::BUY) {
Some(Self::Buy)
} else if matches_bytes(&bytes, &Self::SELL) {
Some(Self::Sell)
} else {
None
}
}
}
const fn matches_bytes(a: &[u8; 32], b: &[u8; 32]) -> bool {
let mut i = 0;
while i < 32 {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
impl Display for OrderKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SellTokenSource {
#[default]
Erc20,
External,
Internal,
}
impl SellTokenSource {
pub const ERC20: [u8; 32] =
hex!("5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9");
pub const EXTERNAL: [u8; 32] =
hex!("abee3b73373acd583a130924aad6dc38cfdc44ba0555ba94ce2ff63980ea0632");
pub const INTERNAL: [u8; 32] =
hex!("4ac99ace14ee0a5ef932dc609df0943ab7ac16b7583634612f8dc35a4289a6ce");
pub const fn as_bytes(&self) -> [u8; 32] {
match self {
Self::Erc20 => Self::ERC20,
Self::External => Self::EXTERNAL,
Self::Internal => Self::INTERNAL,
}
}
pub const fn from_contract_bytes(bytes: [u8; 32]) -> Option<Self> {
if matches_bytes(&bytes, &Self::ERC20) {
Some(Self::Erc20)
} else if matches_bytes(&bytes, &Self::EXTERNAL) {
Some(Self::External)
} else if matches_bytes(&bytes, &Self::INTERNAL) {
Some(Self::Internal)
} else {
None
}
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BuyTokenDestination {
#[default]
Erc20,
Internal,
}
impl BuyTokenDestination {
pub const ERC20: [u8; 32] =
hex!("5a28e9363bb942b639270062aa6bb295f434bcdfc42c97267bf003f272060dc9");
pub const INTERNAL: [u8; 32] =
hex!("4ac99ace14ee0a5ef932dc609df0943ab7ac16b7583634612f8dc35a4289a6ce");
pub const fn as_bytes(&self) -> [u8; 32] {
match self {
Self::Erc20 => Self::ERC20,
Self::Internal => Self::INTERNAL,
}
}
pub const fn from_contract_bytes(bytes: [u8; 32]) -> Option<Self> {
if matches_bytes(&bytes, &Self::ERC20) {
Some(Self::Erc20)
} else if matches_bytes(&bytes, &Self::INTERNAL) {
Some(Self::Internal)
} else {
None
}
}
}
#[derive(Clone, Copy, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct OrderUid(pub [u8; 56]);
impl OrderUid {
pub fn from_parts(hash: B256, owner: Address, valid_to: u32) -> Self {
let mut uid = [0; 56];
uid[0..32].copy_from_slice(hash.as_slice());
uid[32..52].copy_from_slice(owner.as_slice());
uid[52..56].copy_from_slice(&valid_to.to_be_bytes());
Self(uid)
}
pub const fn from_integer(i: u32) -> Self {
let mut uid = [0u8; 56];
let bytes = i.to_be_bytes();
uid[0] = bytes[0];
uid[1] = bytes[1];
uid[2] = bytes[2];
uid[3] = bytes[3];
Self(uid)
}
pub fn parts(&self) -> (B256, Address, u32) {
let mut valid_to = [0u8; 4];
valid_to.copy_from_slice(&self.0[52..56]);
(
B256::from_slice(&self.0[0..32]),
Address::from_slice(&self.0[32..52]),
u32::from_be_bytes(valid_to),
)
}
}
impl Default for OrderUid {
fn default() -> Self {
Self([0u8; 56])
}
}
impl Display for OrderUid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&const_hex::encode_prefixed(self.0))
}
}
impl Debug for OrderUid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
impl FromStr for OrderUid {
type Err = const_hex::FromHexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let body = s
.strip_prefix("0x")
.ok_or(const_hex::FromHexError::InvalidStringLength)?;
let mut value = [0u8; 56];
const_hex::decode_to_slice(body, value.as_mut())?;
Ok(Self(value))
}
}
impl Serialize for OrderUid {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for OrderUid {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct Visitor;
impl de::Visitor<'_> for Visitor {
type Value = OrderUid;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a 56-byte order UID as a 0x-prefixed hex string")
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
OrderUid::from_str(s).map_err(de::Error::custom)
}
}
deserializer.deserialize_str(Visitor)
}
}
#[cfg(test)]
mod tests {
use alloy_primitives::address;
use hex_literal::hex;
use super::*;
use crate::contracts::GPV2_SETTLEMENT as SETTLEMENT;
fn sample_order() -> OrderData {
OrderData {
sell_token: Address::from(hex!("0101010101010101010101010101010101010101")),
buy_token: Address::from(hex!("0202020202020202020202020202020202020202")),
receiver: Some(Address::from(hex!(
"0303030303030303030303030303030303030303"
))),
sell_amount: U256::from(0x0246ddf97976680000_u128),
buy_amount: U256::from(0xb98bc829a6f90000_u128),
valid_to: 0xffffffff,
app_data: AppDataHash([0u8; 32]),
fee_amount: U256::from(0x0de0b6b3a7640000_u128),
kind: OrderKind::Sell,
partially_fillable: false,
sell_token_balance: SellTokenSource::Erc20,
buy_token_balance: BuyTokenDestination::Erc20,
}
}
fn sample_owner() -> Address {
address!("70997970C51812dc3A010C7d01b50e0d17dc79C8")
}
#[test]
fn compute_order_uid_matches_services_golden() {
let domain = DomainSeparator(hex!(
"74e0b11bd18120612556bae4578cfd3a254d7e2495f543c569a92ff5794d9b09"
));
let expected = hex!(
"0e45d31fd31b28c26031cdd81b35a8938b2ccca2cc425fcf440fd3bfed1eede9\
70997970c51812dc3a010c7d01b50e0d17dc79c8\
ffffffff"
);
assert_eq!(sample_order().uid(&domain, sample_owner()).0, expected);
}
#[test]
fn sample_order_struct_hash_matches_ethers() {
assert_eq!(
sample_order().hash_struct(),
hex!("7d9bf070168f9950003bdad00194ef63a5389dd0b594a1288407d551abf147d5")
);
}
#[test]
fn cross_chain_domain_separators_match_ethers() {
let cases: [(u64, [u8; 32]); 11] = [
(
1,
hex!("c078f884a2676e1345748b1feace7b0abee5d00ecadb6e574dcdd109a63e8943"),
),
(
56,
hex!("0cbb18dfca28d2ceac8c72a17289168e03c1ad121338f5573e3b0c3255207fc7"),
),
(
100,
hex!("8f05589c4b810bc2f706854508d66d447cd971f8354a4bb0b3471ceb0a466bc7"),
),
(
137,
hex!("132e0e39721b0cb53216fc42764f69c300d4d21e0caf24e0713b1e3e11120dc2"),
),
(
8453,
hex!("d72ffa789b6fae41254d0b5a13e6e1e92ed947ec6a251edf1cf0b6c02c257b4b"),
),
(
9745,
hex!("e1f9c97768e45812440cd3317c07069178cc2f69971fb204c0211d8bfb1f8e76"),
),
(
42161,
hex!("69d78e7a7cafcaf924483f99f65e8f4e303a99a446db7ab319f9d40e940bced2"),
),
(
43114,
hex!("81fd4ff99b8f80b96c946c146cd5b79181aaf08ecb5808eeee1d047c1de267a5"),
),
(
57073,
hex!("5aced6090755c424bc1d6bbd39a2cdf57e6abfb4663598f4c3c821fb942d52e0"),
),
(
59144,
hex!("b219bb2b8733b80b7ebef0229e7f0c91436f9a0a5b9705fa519237ae0493addb"),
),
(
11_155_111,
hex!("daee378bd0eb30ddf479272accf91761e697bc00e067a268f95f1d2732ed230b"),
),
];
for (chain_id, expected) in cases {
let separator = DomainSeparator::new(chain_id, SETTLEMENT);
assert_eq!(
separator.0, expected,
"domain separator for chain {chain_id}"
);
}
}
#[test]
fn cross_chain_uids_match_ethers() {
const TAIL: [u8; 24] = hex!("70997970c51812dc3a010c7d01b50e0d17dc79c8ffffffff");
let cases: [(u64, [u8; 32]); 11] = [
(
1,
hex!("8295b35c74972663a29a02be0fa8de8a157215b36938caa461fdf183e02cd82e"),
),
(
56,
hex!("f676f63e14dc6a9da6bbe9e57398f060a2cc24d79e12345709ac15c8e4f5b8c1"),
),
(
100,
hex!("3dee66b2accacd71dd607b281d1485ef960c37beff85374f4b7c65eb05ed1252"),
),
(
137,
hex!("f2a78d43cf0922ef45e56a7f36d7eba13a8eb9407c1dc59e087322388e622fbb"),
),
(
8453,
hex!("28862a0c28aab4b8a4403fdf5cfd71686e7dd665db1469ec7f84bf45d1a3dd9b"),
),
(
9745,
hex!("f7941fdf92e8d5b4815995973c1cdf0e58cbe3f404ba4a8f672da5a951832e4c"),
),
(
42161,
hex!("c5677211ea383a13f4d47c092fc48fb3a0a5ade451c82f19dff69a400080f34b"),
),
(
43114,
hex!("310880582f800792d606e89b94c8f23529469003cf71da6aa172737702b8a4be"),
),
(
57073,
hex!("7daedf408aec4bacb29278b0febf3c40ce52c7911d0a23eb5c4c116a8dd44852"),
),
(
59144,
hex!("5aa640f484ef090ab25171d6cbc6adff0cbda7a342ed7ef6371636a1575eca40"),
),
(
11_155_111,
hex!("d69c063b99b74a6690df5541787acc942828219a0ba12fded27eff853da8f6fd"),
),
];
let order = sample_order();
let owner = sample_owner();
for (chain_id, expected_digest) in cases {
let domain = DomainSeparator::new(chain_id, SETTLEMENT);
let uid = order.uid(&domain, owner).0;
let mut expected = [0u8; 56];
expected[0..32].copy_from_slice(&expected_digest);
expected[32..56].copy_from_slice(&TAIL);
assert_eq!(uid, expected, "uid for chain {chain_id}");
}
}
#[test]
fn hash_struct_byte_permutations_match_ethers() {
let mut buy = sample_order();
buy.kind = OrderKind::Buy;
assert_eq!(
buy.hash_struct(),
hex!("7f6ff8bfee1c5f54ca8ac13dabf84e6646592775700fce0e5ead7049620f9ea5")
);
let mut partial = sample_order();
partial.partially_fillable = true;
assert_eq!(
partial.hash_struct(),
hex!("4a7892b4e3cc787cc8dbb22afb249a52b144ae7aec066d2f41f521aa05c7388c")
);
let mut external = sample_order();
external.sell_token_balance = SellTokenSource::External;
assert_eq!(
external.hash_struct(),
hex!("250972eafa5a01e4103f50f3987422339582583b36d2a47e3c6920b4acca3509")
);
let mut internal_sell = sample_order();
internal_sell.sell_token_balance = SellTokenSource::Internal;
assert_eq!(
internal_sell.hash_struct(),
hex!("c94d0a2b1c1b41042d41e0d9f2d05bc91fbe1cb053b716176850029cdb88f679")
);
let mut internal_buy = sample_order();
internal_buy.buy_token_balance = BuyTokenDestination::Internal;
assert_eq!(
internal_buy.hash_struct(),
hex!("4d19213af5ed0adb5ec3d67b00cdcd360ea0f9378a9392f599de132106a558d9")
);
let mut no_receiver = sample_order();
no_receiver.receiver = None;
let mut zero_receiver = sample_order();
zero_receiver.receiver = Some(Address::ZERO);
assert_eq!(no_receiver.hash_struct(), zero_receiver.hash_struct());
assert_eq!(
no_receiver.hash_struct(),
hex!("5388e8a0f9cf9129fd0fd54d3192e502cf5519ee4316f0c77860bfc0c3f42994")
);
}
#[test]
fn order_type_hash_matches_canonical_signature() {
let signature = b"Order(\
address sellToken,\
address buyToken,\
address receiver,\
uint256 sellAmount,\
uint256 buyAmount,\
uint32 validTo,\
bytes32 appData,\
uint256 feeAmount,\
string kind,\
bool partiallyFillable,\
string sellTokenBalance,\
string buyTokenBalance\
)";
assert_eq!(OrderData::TYPE_HASH, *keccak256(signature));
}
#[test]
fn buy_eth_address_matches_canonical_sentinel() {
assert_eq!(
BUY_ETH_ADDRESS,
address!("EeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE")
);
}
#[test]
fn order_kind_keccak_constants() {
assert_eq!(OrderKind::BUY, *keccak256(b"buy"));
assert_eq!(OrderKind::SELL, *keccak256(b"sell"));
}
#[test]
fn sell_token_source_keccak_constants() {
assert_eq!(SellTokenSource::ERC20, *keccak256(b"erc20"));
assert_eq!(SellTokenSource::EXTERNAL, *keccak256(b"external"));
assert_eq!(SellTokenSource::INTERNAL, *keccak256(b"internal"));
}
#[test]
fn buy_token_destination_keccak_constants() {
assert_eq!(BuyTokenDestination::ERC20, *keccak256(b"erc20"));
assert_eq!(BuyTokenDestination::INTERNAL, *keccak256(b"internal"));
}
#[test]
fn order_uid_round_trips_via_string() {
let original = OrderUid::from_str(
"0x5668997bd3fb981d1b3ec44e8483e7c369756df47d10241c1c7a26fde4d1090e89984d17af2f18f8c54873c0de68a56cc5a23e0f695ba915",
)
.unwrap();
let (hash, owner, valid_to) = original.parts();
assert_eq!(
hash,
B256::from(hex!(
"5668997bd3fb981d1b3ec44e8483e7c369756df47d10241c1c7a26fde4d1090e"
))
);
assert_eq!(
owner,
address!("0x89984d17af2f18f8c54873c0de68a56cc5a23e0f")
);
assert_eq!(valid_to, 0x695ba915);
let rebuilt = OrderUid::from_parts(hash, owner, valid_to);
assert_eq!(rebuilt, original);
}
#[test]
fn order_uid_from_str_requires_0x_prefix() {
let bare = "5668997bd3fb981d1b3ec44e8483e7c369756df47d10241c1c7a26fde4d1090e89984d17af2f18f8c54873c0de68a56cc5a23e0f695ba915";
assert!(OrderUid::from_str(bare).is_err());
let prefixed = format!("0x{bare}");
assert!(OrderUid::from_str(&prefixed).is_ok());
}
#[test]
fn order_uid_displays_as_prefixed_hex() {
let mut uid = OrderUid::default();
uid.0[0] = 0x01;
uid.0[55] = 0xff;
let expected = "0x01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff";
assert_eq!(uid.to_string(), expected);
}
#[test]
fn order_uid_pack_matches_contracts_solidity_reference() {
let digest = keccak256(b"order digest");
let owner_seed = keccak256(b"owner");
let owner = Address::from_slice(&owner_seed[12..32]);
let valid_to_seed = keccak256(b"valid to");
let valid_to = u32::from_be_bytes(valid_to_seed[28..32].try_into().unwrap());
let uid = OrderUid::from_parts(digest, owner, valid_to);
let mut expected = [0u8; 56];
expected[0..32].copy_from_slice(digest.as_slice());
expected[32..52].copy_from_slice(owner.as_slice());
expected[52..56].copy_from_slice(&valid_to.to_be_bytes());
assert_eq!(uid.0, expected);
let (round_digest, round_owner, round_valid_to) = uid.parts();
assert_eq!(round_digest, digest);
assert_eq!(round_owner, owner);
assert_eq!(round_valid_to, valid_to);
}
}