Ethers.TypedData (Ethers v0.8.0)

Copy Markdown View Source

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 of type_name (String.t()) to an ordered list of Ethers.TypedData.Field structs describing that struct's members. The synthetic "EIP712Domain" entry must not be included here - it is derived from the domain.
  • primary_type - the name (String.t()) of the top-level struct being signed. Must be a key of types.
  • message - a map of member name to value. Keys are normalized to strings internally.
  • domain - an Ethers.TypedData.Domain struct 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

Types

t()

An EIP-712 typed-data payload.

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

t()

@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 - the Ethers.TypedData.Domain scoping the signature.
  • types - map of struct type name to ordered list of Ethers.TypedData.Field structs.
  • primary_type - the top-level struct type name being signed.
  • message - the message data as a map with string keys.

Functions

domain_separator(typed_data)

@spec domain_separator(t()) :: binary()

Returns the 32-byte EIP-712 domainSeparator (hashStruct("EIP712Domain", domain)).

encode_data(typed_data, type_name, value)

@spec encode_data(t(), String.t(), term()) :: binary()

Returns the EIP-712 encodeData bytes for value as type type_name (typeHash ‖ enc(memberᵢ)…).

encode_type(typed_data, type_name)

@spec encode_type(t(), String.t()) :: String.t()

Returns the EIP-712 encodeType string for type_name (e.g. "Mail(Person from,Person to,string contents)Person(string name,address wallet)").

hash(typed_data)

@spec hash(t()) :: binary()

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.

hash(typed_data, format)

@spec hash(t(), :bin | :hex) :: binary()

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.

hash_struct(typed_data, type_name, value)

@spec hash_struct(t(), String.t(), term()) :: binary()

Returns the 32-byte hashStruct(type_name, value) = keccak256(encodeData(type_name, value)).

new(struct)

@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 to Field structs.
  • :primary_type - the top-level struct type name. Must exist in :types.
  • :domain - an Ethers.TypedData.Domain struct, or a keyword/map accepted by Ethers.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_type is 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{}}.

new(struct, opts)

@spec new(struct(), keyword()) :: {:ok, t()} | {:error, term()}

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"]
)

new!(struct)

@spec new!(struct()) :: t() | no_return()
@spec new!(keyword() | map()) :: t() | no_return()

Same as new/1 but raises an ArgumentError on error.

new!(struct, opts)

@spec new!(struct(), keyword()) :: t() | no_return()

Same as new/2 but raises an ArgumentError on error.

recover_signer(typed_data, signature)

@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 0x address 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"

to_eip712_json(typed_data)

@spec to_eip712_json(t()) :: %{required(String.t()) => term()}

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 - 0x checksummed hex (accepts a 0x-hex string or a 20-byte binary).
  • bytesN / bytes - 0x hex (accepts a 0x-hex string or a binary).
  • uintN / intN - decimal string (accepts an integer, a decimal string, or a 0x-hex string). Always serialized as a decimal string for full uint256-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.

type_hash(typed_data, type_name)

@spec type_hash(t(), String.t()) :: binary()

Returns the 32-byte typeHash (keccak256(encodeType(type_name))).

valid_signature?(typed_data, signature, expected_address)

@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