use crate::SolValue;
use alloc::{borrow::Cow, string::String, vec::Vec};
use alloy_primitives::{Address, B256, FixedBytes, U256, keccak256};
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "eip712-serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "eip712-serde", serde(rename_all = "camelCase"))]
pub struct Eip712Domain {
#[cfg_attr(feature = "eip712-serde", serde(default, skip_serializing_if = "Option::is_none"))]
pub name: Option<Cow<'static, str>>,
#[cfg_attr(feature = "eip712-serde", serde(default, skip_serializing_if = "Option::is_none"))]
pub version: Option<Cow<'static, str>>,
#[cfg_attr(feature = "eip712-serde", serde(default, skip_serializing_if = "Option::is_none"))]
pub chain_id: Option<U256>,
#[cfg_attr(feature = "eip712-serde", serde(default, skip_serializing_if = "Option::is_none"))]
pub verifying_contract: Option<Address>,
#[cfg_attr(feature = "eip712-serde", serde(default, skip_serializing_if = "Option::is_none"))]
pub salt: Option<B256>,
}
impl Eip712Domain {
pub const NAME: &'static str = "EIP712Domain";
#[inline]
pub const fn new(
name: Option<Cow<'static, str>>,
version: Option<Cow<'static, str>>,
chain_id: Option<U256>,
verifying_contract: Option<Address>,
salt: Option<B256>,
) -> Self {
Self { name, version, chain_id, verifying_contract, salt }
}
#[inline]
pub fn separator(&self) -> B256 {
self.hash_struct()
}
pub fn encode_type(&self) -> String {
macro_rules! encode_type {
($($field:ident => $repr:literal),+ $(,)?) => {
let mut ty = String::with_capacity(Self::NAME.len() + 2 $(+ $repr.len() * self.$field.is_some() as usize)+);
ty.push_str(Self::NAME);
ty.push('(');
$(
if self.$field.is_some() {
ty.push_str($repr);
}
)+
if ty.ends_with(',') {
ty.pop();
}
ty.push(')');
ty
};
}
encode_type! {
name => "string name,",
version => "string version,",
chain_id => "uint256 chainId,",
verifying_contract => "address verifyingContract,",
salt => "bytes32 salt",
}
}
#[inline]
pub fn type_hash(&self) -> B256 {
keccak256(self.encode_type().as_bytes())
}
#[inline]
pub const fn num_words(&self) -> usize {
self.name.is_some() as usize
+ self.version.is_some() as usize
+ self.chain_id.is_some() as usize
+ self.verifying_contract.is_some() as usize
+ self.salt.is_some() as usize
}
#[inline]
pub const fn abi_encoded_size(&self) -> usize {
self.num_words() * 32
}
pub fn encode_data_to(&self, out: &mut Vec<u8>) {
macro_rules! encode_opt {
($opt:expr) => {
if let Some(t) = $opt {
out.extend_from_slice(t.tokenize().as_slice());
}
};
}
#[inline]
#[allow(clippy::ptr_arg)]
fn cow_keccak256(s: &Cow<'_, str>) -> FixedBytes<32> {
keccak256(s.as_bytes())
}
out.reserve(self.abi_encoded_size());
encode_opt!(self.name.as_ref().map(cow_keccak256));
encode_opt!(self.version.as_ref().map(cow_keccak256));
encode_opt!(&self.chain_id);
encode_opt!(&self.verifying_contract);
encode_opt!(&self.salt);
}
pub fn encode_data(&self) -> Vec<u8> {
let mut out = Vec::new();
self.encode_data_to(&mut out);
out
}
#[inline]
pub fn hash_struct(&self) -> B256 {
let mut hasher = alloy_primitives::Keccak256::new();
hasher.update(self.type_hash());
hasher.update(self.encode_data());
hasher.finalize()
}
}
#[macro_export]
macro_rules! eip712_domain {
(@opt) => { $crate::private::None };
(@opt $e:expr) => { $crate::private::Some($e) };
(@cow) => { $crate::private::None };
(@cow $l:literal) => { $crate::private::Some($crate::private::Cow::Borrowed($l)) };
(@cow $e:expr) => { $crate::private::Some(<$crate::private::Cow<'static, str> as $crate::private::From<_>>::from($e)) };
(
$(name: $name:expr,)?
$(version: $version:expr,)?
$(chain_id: $chain_id:expr,)?
$(verifying_contract: $verifying_contract:expr,)?
$(salt: $salt:expr)?
$(,)?
) => {
$crate::Eip712Domain::new(
$crate::eip712_domain!(@cow $($name)?),
$crate::eip712_domain!(@cow $($version)?),
$crate::eip712_domain!(@opt $($crate::private::u256($chain_id))?),
$crate::eip712_domain!(@opt $($verifying_contract)?),
$crate::eip712_domain!(@opt $($salt)?),
)
};
}
#[cfg(test)]
mod tests {
use super::*;
const _: Eip712Domain = eip712_domain! {
name: "abcd",
};
const _: Eip712Domain = eip712_domain! {
name: "abcd",
version: "1",
};
const _: Eip712Domain = eip712_domain! {
name: "abcd",
version: "1",
chain_id: 1,
};
const _: Eip712Domain = eip712_domain! {
name: "abcd",
version: "1",
chain_id: 1,
verifying_contract: Address::ZERO,
};
const _: Eip712Domain = eip712_domain! {
name: "abcd",
version: "1",
chain_id: 1,
verifying_contract: Address::ZERO,
salt: B256::ZERO };
const _: Eip712Domain = eip712_domain! {
name: "abcd",
version: "1",
chain_id: 1,
verifying_contract: Address::ZERO,
salt: B256::ZERO, };
const _: Eip712Domain = eip712_domain! {
name: "abcd",
version: "1",
verifying_contract: Address::ZERO,
salt: B256::ZERO,
};
const _: Eip712Domain = eip712_domain! {
name: "abcd",
chain_id: 1,
verifying_contract: Address::ZERO,
salt: B256::ZERO,
};
const _: Eip712Domain = eip712_domain! {
name: "abcd",
verifying_contract: Address::ZERO,
salt: B256::ZERO,
};
const _: Eip712Domain = eip712_domain! {
name: "abcd",
salt: B256::ZERO,
};
const _: Eip712Domain = eip712_domain! {
version: "1",
salt: B256::ZERO,
};
const _: Eip712Domain = eip712_domain! {
version: "1",
verifying_contract: Address::ZERO,
salt: B256::ZERO,
};
#[test]
fn runtime_domains() {
let _: Eip712Domain = eip712_domain! {
name: String::new(),
version: String::new(),
};
let my_string = String::from("!@#$%^&*()_+");
let _: Eip712Domain = eip712_domain! {
name: my_string.clone(),
version: my_string,
};
let my_cow = Cow::from("my_cow");
let _: Eip712Domain = eip712_domain! {
name: my_cow.clone(),
version: my_cow.into_owned(),
};
}
}