Models an EIP-712 typed structured data payload.
An Ethers.TypedData mirrors the canonical eth_signTypedData_v4 JSON shape so the same
struct serves both hashing (the local signing pipeline) and JSON-RPC signing. It is built from:
types- a map oftype_name(String.t()) to an ordered list ofEthers.TypedData.Fieldstructs describing that struct's members. The synthetic"EIP712Domain"entry must not be included here - it is derived from thedomain.primary_type- the name (String.t()) of the top-level struct being signed. Must be a key oftypes.message- a map of member name to value. Keys are normalized to strings internally.domain- anEthers.TypedData.Domainstruct scoping the signature.
Example (the canonical Mail/Person example)
The following builds the Mail message from the EIP-712 specification and reproduces the
reference encodeType string, domain separator and signing digest published by the spec:
iex> typed_data =
...> Ethers.TypedData.new!(
...> types: %{
...> "Person" => [
...> %{name: "name", type: "string"},
...> %{name: "wallet", type: "address"}
...> ],
...> "Mail" => [
...> %{name: "from", type: "Person"},
...> %{name: "to", type: "Person"},
...> %{name: "contents", type: "string"}
...> ]
...> },
...> primary_type: "Mail",
...> domain: [
...> name: "Ether Mail",
...> version: "1",
...> chain_id: 1,
...> verifying_contract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
...> ],
...> message: %{
...> "from" => %{"name" => "Cow", "wallet" => "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},
...> "to" => %{"name" => "Bob", "wallet" => "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},
...> "contents" => "Hello, Bob!"
...> }
...> )
iex> Ethers.TypedData.encode_type(typed_data, "Mail")
"Mail(Person from,Person to,string contents)Person(string name,address wallet)"
iex> Ethers.TypedData.hash(typed_data, :hex)
"0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2"
iex> Ethers.Utils.hex_encode(Ethers.TypedData.domain_separator(typed_data))
"0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f"
Summary
Functions
Returns the 32-byte EIP-712 domainSeparator (hashStruct("EIP712Domain", domain)).
Returns the EIP-712 encodeData bytes for value as type type_name
(typeHash ‖ enc(memberᵢ)…).
Returns the EIP-712 encodeType string for type_name (e.g.
"Mail(Person from,Person to,string contents)Person(string name,address wallet)").
Returns the EIP-712 signing digest
keccak256(0x19 ‖ 0x01 ‖ domainSeparator ‖ hashStruct(primaryType, message)).
Returns the EIP-712 signing digest of typed_data in the requested format.
Returns the 32-byte hashStruct(type_name, value) = keccak256(encodeData(type_name, value)).
Builds an Ethers.TypedData from the given parameters, normalizing and validating them.
Builds an Ethers.TypedData from a schema struct instance (see Ethers.TypedData.Schema).
Same as new/1 but raises an ArgumentError on error.
Same as new/2 but raises an ArgumentError on error.
Recovers the signer address from an EIP-712 typed-data payload and its signature.
Serializes an Ethers.TypedData into the canonical eth_signTypedData_v4 JSON-compatible map.
Returns the 32-byte typeHash (keccak256(encodeType(type_name))).
Checks whether signature over typed_data was produced by expected_address.
Types
@type t() :: %Ethers.TypedData{ domain: Ethers.TypedData.Domain.t(), message: %{required(String.t()) => term()}, primary_type: String.t(), types: %{required(String.t()) => [Ethers.TypedData.Field.t()]} }
An EIP-712 typed-data payload.
domain- theEthers.TypedData.Domainscoping the signature.types- map of struct type name to ordered list ofEthers.TypedData.Fieldstructs.primary_type- the top-level struct type name being signed.message- the message data as a map with string keys.
Functions
Returns the 32-byte EIP-712 domainSeparator (hashStruct("EIP712Domain", domain)).
Returns the EIP-712 encodeData bytes for value as type type_name
(typeHash ‖ enc(memberᵢ)…).
Returns the EIP-712 encodeType string for type_name (e.g.
"Mail(Person from,Person to,string contents)Person(string name,address wallet)").
Returns the EIP-712 signing digest
keccak256(0x19 ‖ 0x01 ‖ domainSeparator ‖ hashStruct(primaryType, message)).
With :bin (default) returns the raw 32-byte binary; with :hex returns 0x-prefixed hex.
Returns the EIP-712 signing digest of typed_data in the requested format.
With :bin returns the raw 32-byte binary; with :hex returns a 0x-prefixed hex string.
See hash/1 for the digest definition.
Returns the 32-byte hashStruct(type_name, value) = keccak256(encodeData(type_name, value)).
@spec new(struct()) :: {:ok, t()} | {:error, term()}
@spec new(keyword() | map()) :: {:ok, t()} | {:error, term()}
Builds an Ethers.TypedData from the given parameters, normalizing and validating them.
Parameters (keyword list or map)
:types- a map of type name to a list of field definitions. Each field may be a%Ethers.TypedData.Field{}, a map with atom keys (%{name: ..., type: ...}), or a map with string keys (%{"name" => ..., "type" => ...}). All are normalized toFieldstructs.:primary_type- the top-level struct type name. Must exist in:types.:domain- anEthers.TypedData.Domainstruct, or a keyword/map accepted byEthers.TypedData.Domain.new/1.:message- a map of member name to value. Both atom and string keys are accepted and normalized to string keys.
Validation
Returns {:error, reason} when:
:primary_typeis not a key of:types({:error, {:unknown_primary_type, name}}).- a member references a struct type that is not defined in
:types({:error, {:undefined_type, name}}).
Otherwise returns {:ok, %Ethers.TypedData{}}.
Builds an Ethers.TypedData from a schema struct instance (see Ethers.TypedData.Schema).
Declare the EIP-712 struct types as modules with use Ethers.TypedData.Schema, then pass an
instance of the top-level struct. The struct is expanded into types/primary_type/message
(walking referenced schema modules transitively) and validated via new/1, so the result is
identical to the equivalent map-based new/1 call.
opts may carry :domain (a keyword/map accepted by Ethers.TypedData.Domain.new/1).
Example
# given `Person`/`Mail` schema modules (see `Ethers.TypedData.Schema`)
Ethers.TypedData.new!(
%Mail{
from: %Person{name: "Cow", wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},
to: %Person{name: "Bob", wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},
contents: "Hello, Bob!"
},
domain: [name: "Ether Mail", version: "1", chain_id: 1,
verifying_contract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"]
)
Same as new/1 but raises an ArgumentError on error.
Same as new/2 but raises an ArgumentError on error.
@spec recover_signer(t(), binary()) :: Ethers.Types.t_address() | {:error, term()}
Recovers the signer address from an EIP-712 typed-data payload and its signature.
The signing digest is recomputed from typed_data (via hash/1) and the signer's public key
is recovered from the signature, then converted to its checksummed 0x address.
signature may be given as a 0x-prefixed hex string (130 hex chars) or a raw 65-byte binary
(r ‖ s ‖ v).
Recovery is ecrecover-based and therefore works for externally-owned accounts (EOAs)
only — smart-contract wallets (ERC-1271/ERC-6492) do not have a recoverable signer.
Returns
- a checksummed
0xaddress string on success. {:error, reason}if the public key could not be recovered.
Examples
{:ok, sig} =
Ethers.sign_typed_data(typed_data,
signer: Ethers.Signer.Local,
signer_opts: [private_key: key]
)
Ethers.TypedData.recover_signer(typed_data, sig)
#=> "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
Serializes an Ethers.TypedData into the canonical eth_signTypedData_v4 JSON-compatible map.
The returned map uses string keys and JSON-ready scalar values so it can be encoded with
Jason.encode!/1 and handed to a wallet / node. Its shape is:
%{
"types" => %{
"EIP712Domain" => [%{"name" => ..., "type" => ...}, ...],
<primary type and every referenced type> => [%{"name" => ..., "type" => ...}, ...]
},
"primaryType" => "Mail",
"domain" => %{...present domain fields...},
"message" => %{...}
}Value serialization
address-0xchecksummed hex (accepts a0x-hex string or a 20-byte binary).bytesN/bytes-0xhex (accepts a0x-hex string or a binary).uintN/intN- decimal string (accepts an integer, a decimal string, or a0x-hex string). Always serialized as a decimal string for fulluint256-range interoperability.bool/string- passed through unchanged.- reference struct types - recursed into as nested maps.
- array types (
T[]/T[n], including nested arrays) - serialized as lists element by element.
The "EIP712Domain" entry is derived from the domain's present (non-nil) fields in canonical
order; the caller must not include it in types.
Returns the 32-byte typeHash (keccak256(encodeType(type_name))).
@spec valid_signature?(t(), binary(), Ethers.Types.t_address()) :: boolean()
Checks whether signature over typed_data was produced by expected_address.
Recovers the signer via recover_signer/2 and compares it to expected_address. The
comparison is done on the decoded 20-byte addresses, so checksum/case differences are ignored.
signature may be a 0x-prefixed hex string or a raw 65-byte binary.
EOA-only — signatures from smart-contract wallets (ERC-1271/ERC-6492) always fail this
check. Prefer Ethers.Signature.verify_typed_data/4, which verifies EOA and
smart-contract wallet signatures alike; use this function when you specifically need a
pure, RPC-free check for signatures known to come from EOA keys.
Examples
Ethers.TypedData.valid_signature?(typed_data, sig, "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1")
#=> true