# EIP-712 Typed Data

[EIP-712](https://eips.ethereum.org/EIPS/eip-712) is the standard for hashing and signing
**typed structured data** rather than opaque byte strings. Instead of asking a user to sign an
unreadable hash, a wallet can show the actual fields being signed (a mail message, an order, a
permit, ...) and produce a signature that a smart contract can verify on-chain.

Ethers models an EIP-712 payload with `Ethers.TypedData` and lets you hash it, sign it (with the
`Ethers.Signer.Local` or `Ethers.Signer.JsonRPC` signers) and recover/verify the signer.

## The pieces

An EIP-712 payload is made of four parts:

- **`types`** - a map of struct type name to its ordered members. Each member is a
  `%{name: ..., type: ...}` pair (normalized to `Ethers.TypedData.Field` structs). Reference other
  structs by name (`"Person"`), and use array suffixes (`"Person[]"`) just like Solidity.
- **`primary_type`** - the name of the top-level struct that is being signed.
- **`domain`** - an `Ethers.TypedData.Domain` scoping the signature to an app/contract/chain so a
  signature cannot be replayed elsewhere. All five fields (`name`, `version`, `chain_id`,
  `verifying_contract`, `salt`) are optional; only the present ones participate.
- **`message`** - the actual values, as a map keyed by member name.

You do **not** declare the synthetic `"EIP712Domain"` type yourself - Ethers derives it from the
`domain` you pass.

## Building an `Ethers.TypedData`

This guide uses the canonical `Mail`/`Person` example from the EIP-712 specification so every
intermediate value can be cross-checked against the spec.

```elixir
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!"
    }
  )
```

`new/1` (and the raising `new!/1`) normalizes the input - field maps become
`Ethers.TypedData.Field` structs, message keys become strings, and the `domain` becomes an
`Ethers.TypedData.Domain` - and validates that `primary_type` and every referenced struct type is
defined.

## Defining typed data as Elixir structs

Instead of hand-writing the `types` and `message` maps you can declare each EIP-712 struct type as
an Elixir module with `use Ethers.TypedData.Schema`, then build the payload from struct instances.
The `typed_schema`/`field` macros generate a matching `defstruct`, and field order is preserved as
the source of truth for the order-sensitive `encodeType` string.

```elixir
defmodule Person do
  use Ethers.TypedData.Schema

  typed_schema "Person" do
    field :name, :string
    field :wallet, :address
  end
end

defmodule Mail do
  use Ethers.TypedData.Schema

  typed_schema "Mail" do
    field :from, Person
    field :to, Person
    field :contents, :string
  end
end
```

A field's type can be another schema module (`Person`), an atom Solidity type (`:string`,
`:address`, `:uint256`), a literal type string (`"bytes32"`), or an array form (`{:array, Person}`
for `"Person[]"`, `{:array, inner, n}` for a fixed-size array). Referenced schema modules are
walked recursively to build the full `types` map.

Build the payload with `Ethers.TypedData.new!/2` (or `new/2`), passing the top-level struct and a
`:domain`:

```elixir
typed_data =
  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"
    ]
  )
```

The struct is expanded into the same `types`/`primary_type`/`message` values and validated through
`new/1`, so this produces exactly the same `Ethers.TypedData` - and therefore the same
`encodeType`, domain separator and digest - as the map-based example above. See
`Ethers.TypedData.Schema` for the full DSL reference.

## Hashing

`Ethers.TypedData` exposes each step of the EIP-712 hashing algorithm:

```elixir
# The `encodeType` string of a struct (primary type first, referenced types sorted alphabetically)
Ethers.TypedData.encode_type(typed_data, "Mail")
#=> "Mail(Person from,Person to,string contents)Person(string name,address wallet)"

# The 32-byte domain separator (hashStruct of the EIP712Domain)
Ethers.TypedData.domain_separator(typed_data) |> Ethers.Utils.hex_encode()
#=> "0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f"

# The final signing digest: keccak256(0x19 0x01 ‖ domainSeparator ‖ hashStruct(primaryType, message))
Ethers.TypedData.hash(typed_data)          # raw 32-byte binary
Ethers.TypedData.hash(typed_data, :hex)
#=> "0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2"
```

Both the `encodeType` string and the digest above match the reference values published in the
EIP-712 specification.

## Signing

### With the Local signer

`Ethers.Signer.Local` signs the digest locally with a private key. It requires the optional
`ex_secp256k1` dependency (see the README installation notes).

```elixir
{:ok, signature} =
  Ethers.sign_typed_data(typed_data,
    signer: Ethers.Signer.Local,
    signer_opts: [
      private_key: "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d",
      from: "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
    ]
  )

signature
#=> "0x..." (a 0x-prefixed 65-byte signature: r ‖ s ‖ v)
```

The `from` address is validated against the private key; a mismatch returns `{:error, :wrong_key}`.

### With the JsonRPC signer

`Ethers.Signer.JsonRPC` delegates to the node/wallet via the `eth_signTypedData_v4` RPC method
(supported by anvil, geth, [web3signer](https://github.com/Consensys/web3signer), ...). The account
must be managed by that endpoint - no private key is passed:

```elixir
{:ok, signature} =
  Ethers.sign_typed_data(typed_data,
    signer: Ethers.Signer.JsonRPC,
    signer_opts: [from: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"]
  )
```

Under the hood the payload is serialized to the canonical `eth_signTypedData_v4` JSON shape via
`Ethers.TypedData.to_eip712_json/1` (integers as decimal strings, addresses/bytes as `0x` hex).

## Recovering and verifying

Given a signature you can recover the signing address or check it against an expected one. These
helpers live on `Ethers.TypedData`:

```elixir
Ethers.TypedData.recover_signer(typed_data, signature)
#=> "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"

Ethers.TypedData.valid_signature?(
  typed_data,
  signature,
  "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
)
#=> true
```

`recover_signer/2` and `valid_signature?/3` accept the signature as either a `0x`-prefixed hex
string or a raw 65-byte binary. The address comparison in `valid_signature?/3` is done on the
decoded 20-byte addresses, so checksum/case differences are ignored.
