use core::fmt;
use eth_valkyoth_hash::{Keccak256, hash_one};
use eth_valkyoth_primitives::B256;
use std::string::{String, ToString};
use std::vec::Vec;
#[path = "eip712_json_dom.rs"]
mod dom;
#[path = "eip712_json_scalar.rs"]
mod scalar;
use dom::{Json, Object};
use scalar::{
check_depth, check_len, encode_json_numeric_or_bytes, parse_address, parse_b256, parse_chain_id,
};
use super::typed_helpers::{find_struct, parse_array_type};
use super::{
ADDRESS_PADDING_BYTES, Eip712DomainData, Eip712EncodeError, Eip712Field, Eip712StructType,
MAX_TYPE_DEPTH, WORD_BYTES, eip712_domain_separator, eip712_type_hash,
};
use crate::eip712_signing_digest;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Eip712JsonLimits {
pub max_input_bytes: usize,
pub max_types: usize,
pub max_fields_per_type: usize,
pub max_array_items: usize,
pub max_string_bytes: usize,
pub max_bytes_value: usize,
pub max_depth: usize,
}
impl Eip712JsonLimits {
pub const DEFAULT: Self = Self {
max_input_bytes: 16_384,
max_types: 64,
max_fields_per_type: 64,
max_array_items: 256,
max_string_bytes: 4096,
max_bytes_value: 4096,
max_depth: MAX_TYPE_DEPTH,
};
}
#[non_exhaustive]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Eip712JsonError {
Json,
Shape,
Limit,
Hex,
Integer,
Encode(Eip712EncodeError),
}
impl fmt::Display for Eip712JsonError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Json => "EIP-712 JSON is malformed or has duplicate keys",
Self::Shape => "EIP-712 JSON document has an unsupported shape",
Self::Limit => "EIP-712 JSON parser limit was exceeded",
Self::Hex => "EIP-712 JSON hex string is invalid",
Self::Integer => "EIP-712 JSON integer is invalid or too large",
Self::Encode(error) => return error.fmt(formatter),
})
}
}
#[cfg(feature = "std")]
impl std::error::Error for Eip712JsonError {}
impl From<Eip712EncodeError> for Eip712JsonError {
fn from(error: Eip712EncodeError) -> Self {
Self::Encode(error)
}
}
pub fn eip712_json_typed_data_signing_digest<H>(
input: &str,
limits: Eip712JsonLimits,
type_scratch: &mut [u8],
) -> Result<B256, Eip712JsonError>
where
H: Default + Keccak256,
{
check_len(input.len(), limits.max_input_bytes)?;
let json: Json = serde_json::from_str(input).map_err(|_| Eip712JsonError::Json)?;
let root = json.as_object()?;
let owned = parse_types(root.get("types")?, limits)?;
let field_sets = borrow_field_sets(&owned);
let types = borrow_types(&owned, &field_sets);
let primary_type = root.get("primaryType")?.as_str()?;
let domain = parse_domain(root.get("domain")?, limits)?;
let message = root.get("message")?;
let domain_separator = eip712_domain_separator::<H>(domain, type_scratch)?;
let message_hash =
hash_json_struct::<H>(&types, primary_type, message, type_scratch, limits, 0)?;
Ok(eip712_signing_digest(
domain_separator,
message_hash,
H::default(),
))
}
fn hash_json_struct<H>(
types: &[Eip712StructType<'_>],
primary_type: &str,
value: &Json,
type_scratch: &mut [u8],
limits: Eip712JsonLimits,
depth: usize,
) -> Result<B256, Eip712JsonError>
where
H: Default + Keccak256,
{
check_depth(depth, limits)?;
let object = value.as_object()?;
let ty = find_struct(types, primary_type)?;
let type_hash = eip712_type_hash::<H>(types, primary_type, type_scratch)?;
let mut hasher = H::default();
hasher.update(&type_hash.to_bytes());
for field in ty.fields {
let mut word = [0_u8; WORD_BYTES];
encode_json_word::<H>(
types,
field.type_name,
object.get(field.name)?,
&mut word,
type_scratch,
limits,
depth.saturating_add(1),
)?;
hasher.update(&word);
}
Ok(hasher.finalize())
}
fn encode_json_word<H>(
types: &[Eip712StructType<'_>],
type_name: &str,
value: &Json,
out: &mut [u8; WORD_BYTES],
type_scratch: &mut [u8],
limits: Eip712JsonLimits,
depth: usize,
) -> Result<(), Eip712JsonError>
where
H: Default + Keccak256,
{
if let Some(array) = parse_array_type(type_name)? {
check_depth(depth, limits)?;
let values = value.as_array()?;
check_len(values.len(), limits.max_array_items)?;
if let Some(expected) = array.len
&& values.len() != expected
{
return Err(Eip712JsonError::Shape);
}
let mut hasher = H::default();
for item in values {
let mut word = [0_u8; WORD_BYTES];
encode_json_word::<H>(
types,
array.base,
item,
&mut word,
type_scratch,
limits,
depth.saturating_add(1),
)?;
hasher.update(&word);
}
*out = hasher.finalize().to_bytes();
return Ok(());
}
match type_name {
"bool" => {
let last = out.last_mut().ok_or(Eip712EncodeError::OutputTooShort)?;
*last = u8::from(value.as_bool()?);
}
"address" => {
let address = parse_address(value.as_str()?)?;
out.get_mut(ADDRESS_PADDING_BYTES..)
.ok_or(Eip712EncodeError::OutputTooShort)?
.copy_from_slice(&address.to_bytes());
}
"bytes" => {
let bytes = scalar::parse_hex(value.as_str()?, limits.max_bytes_value)?;
*out = hash_one(H::default(), &bytes).to_bytes();
}
"string" => {
let text = value.as_str()?;
check_len(text.len(), limits.max_string_bytes)?;
*out = hash_one(H::default(), text.as_bytes()).to_bytes();
}
name if find_struct(types, name).is_ok() => {
*out =
hash_json_struct::<H>(types, name, value, type_scratch, limits, depth)?.to_bytes();
}
name => encode_json_numeric_or_bytes(name, value, out, limits)?,
}
Ok(())
}
struct OwnedType {
name: String,
fields: Vec<OwnedField>,
}
struct OwnedField {
name: String,
type_name: String,
}
fn parse_types(types: &Json, limits: Eip712JsonLimits) -> Result<Vec<OwnedType>, Eip712JsonError> {
let object = types.as_object()?;
let mut owned = Vec::new();
for (name, fields) in object.entries() {
if name == "EIP712Domain" {
check_len(fields.as_array()?.len(), limits.max_fields_per_type)?;
continue;
}
check_len(owned.len().saturating_add(1), limits.max_types)?;
check_len(name.len(), limits.max_string_bytes)?;
let field_values = fields.as_array()?;
check_len(field_values.len(), limits.max_fields_per_type)?;
let mut parsed_fields = Vec::<OwnedField>::new();
for field in field_values {
let field = field.as_object()?;
let field_name = field.get("name")?.as_str()?;
let field_type = field.get("type")?.as_str()?;
check_len(field_name.len(), limits.max_string_bytes)?;
check_len(field_type.len(), limits.max_string_bytes)?;
if parsed_fields.iter().any(|field| field.name == field_name) {
return Err(Eip712JsonError::Shape);
}
parsed_fields.push(OwnedField {
name: field_name.to_string(),
type_name: field_type.to_string(),
});
}
owned.push(OwnedType {
name: name.to_string(),
fields: parsed_fields,
});
}
Ok(owned)
}
fn borrow_field_sets(types: &[OwnedType]) -> Vec<Vec<Eip712Field<'_>>> {
types
.iter()
.map(|ty| {
ty.fields
.iter()
.map(|field| Eip712Field {
name: field.name.as_str(),
type_name: field.type_name.as_str(),
})
.collect()
})
.collect()
}
fn borrow_types<'a>(
types: &'a [OwnedType],
field_sets: &'a [Vec<Eip712Field<'a>>],
) -> Vec<Eip712StructType<'a>> {
types
.iter()
.zip(field_sets.iter())
.map(|(ty, fields)| Eip712StructType {
name: ty.name.as_str(),
fields,
})
.collect()
}
fn parse_domain(
domain: &Json,
limits: Eip712JsonLimits,
) -> Result<Eip712DomainData<'_>, Eip712JsonError> {
let object = domain.as_object()?;
reject_unknown_domain_fields(object)?;
let name = optional_limited_str(object.get_optional("name"), limits)?;
let version = optional_limited_str(object.get_optional("version"), limits)?;
let chain_id = object
.get_optional("chainId")
.map(parse_chain_id)
.transpose()?;
let verifying_contract = object
.get_optional("verifyingContract")
.map(|value| parse_address(value.as_str()?))
.transpose()?;
let salt = object
.get_optional("salt")
.map(|value| parse_b256(value.as_str()?))
.transpose()?;
Ok(Eip712DomainData {
name,
version,
chain_id,
verifying_contract,
salt,
})
}
fn reject_unknown_domain_fields(object: &Object) -> Result<(), Eip712JsonError> {
for (field, _) in object.entries() {
if !matches!(
field,
"name" | "version" | "chainId" | "verifyingContract" | "salt"
) {
return Err(Eip712JsonError::Shape);
}
}
Ok(())
}
fn optional_limited_str(
value: Option<&Json>,
limits: Eip712JsonLimits,
) -> Result<Option<&str>, Eip712JsonError> {
let Some(value) = value else {
return Ok(None);
};
let text = value.as_str()?;
check_len(text.len(), limits.max_string_bytes)?;
Ok(Some(text))
}
#[cfg(test)]
#[path = "eip712_json_tests.rs"]
mod tests;