Ethers.TypedData.Schema (Ethers v0.7.0)

Copy Markdown View Source

Compile-time DSL for declaring EIP-712 typed-data struct types as native Elixir modules.

Instead of hand-writing the types/primary_type/message maps that Ethers.TypedData.new/1 consumes, you declare each EIP-712 struct type as an Elixir module and build an Ethers.TypedData straight from struct instances with Ethers.TypedData.new/2 (or new!/2). This is a thin front-end over the existing engine - the resulting payload hashes and serializes identically to the map-based form.

Usage

use Ethers.TypedData.Schema imports the typed_schema/1,2 and field/2,3 macros. A typed_schema block declares an ordered list of fields; the macro generates a matching defstruct plus an introspection function __typed_data_schema__/0.

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

typed_schema/1 derives the EIP-712 type name from the module's last segment (MyApp.Messages.Mail -> "Mail"); typed_schema/2 takes an explicit name string.

Field types

The second argument to field/2,3 is the member's type, in one of these forms:

  • a schema module reference (Person) - another module that uses this DSL. The EIP-712 type string is that schema's declared type name, and it is registered (recursively) in the generated types map.
  • an atom (:string, :address, :uint256) - an atomic Solidity type; the type string is Atom.to_string/1 of it.
  • a string ("uint256", "bytes32") - a literal EIP-712 type string.
  • {:array, inner} - a dynamic array of inner ("<inner>[]").
  • {:array, inner, n} - a fixed-size array of inner ("<inner>[n]").

Field order matters - EIP-712 encodeType is order-sensitive, so the sequence of field/2,3 calls is the source of truth (which is why the block also generates the defstruct). Type references (a schema module atom such as Person) are stored unresolved and resolved at runtime by the expander, not at macro-expansion time - so mutually referencing modules do not create compile-ordering problems.

Building typed data from a schema

Given the Person/Mail schemas above, build and hash the canonical EIP-712 Mail message. (These example modules ship in the test suite as Ethers.Support.EIP712.Person and Ethers.Support.EIP712.Mail.)

iex> alias Ethers.Support.EIP712.{Mail, Person}
iex> mail = %Mail{
...>   from: %Person{name: "Cow", wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},
...>   to: %Person{name: "Bob", wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},
...>   contents: "Hello, Bob!"
...> }
iex> td =
...>   Ethers.TypedData.new!(mail,
...>     domain: [
...>       name: "Ether Mail",
...>       version: "1",
...>       chain_id: 1,
...>       verifying_contract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
...>     ]
...>   )
iex> Ethers.TypedData.encode_type(td, "Mail")
"Mail(Person from,Person to,string contents)Person(string name,address wallet)"
iex> Ethers.TypedData.hash(td, :hex)
"0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2"

Introspection contract

The generated __typed_data_schema__/0 returns:

{type_name :: String.t(), [%{key: atom(), name: String.t(), type: term()}]}

where each type term is the raw declared type: a module atom (Person), an atom (:string), a string ("uint256"), or {:array, inner} / {:array, inner, n}. The per-field :default (used only for defstruct) is intentionally dropped from this tuple.

Summary

Functions

Declares a single field of the enclosing typed_schema.

Expands a schema struct instance into the parameters consumed by Ethers.TypedData.new/1.

Declares an EIP-712 typed-data schema.

Functions

field(key, type, opts \\ [])

(macro)

Declares a single field of the enclosing typed_schema.

  • key - the struct key (must be an atom). Declaring the same key twice raises.
  • type - the raw, unresolved type term: a schema module atom (Person), an atom (:string, :uint256), a type string ("uint256"), or {:array, inner} / {:array, inner, n}.
  • opts:
    • :name - the EIP-712 member name (defaults to to_string(key)).
    • :default - the defstruct default for this key (defaults to nil).

to_params(struct, opts)

@spec to_params(struct(), keyword()) :: %{
  types: %{required(String.t()) => [Ethers.TypedData.Field.t()]},
  primary_type: String.t(),
  message: %{required(String.t()) => term()},
  domain: keyword() | map()
}

Expands a schema struct instance into the parameters consumed by Ethers.TypedData.new/1.

Walks the schema module referenced by struct transitively, resolving every referenced type into the EIP-712 types map and building the string-keyed message from the struct instance.

Returns %{types:, primary_type:, message:, domain:} ready to hand to Ethers.TypedData.new/1.

Raises ArgumentError if struct is not a schema module (does not export __typed_data_schema__/0), if a referenced type is not a schema module, or if two distinct modules resolve to the same EIP-712 type name with different fields.

typed_schema(type_name \\ nil, list)

(macro)

Declares an EIP-712 typed-data schema.

typed_schema/1 derives the EIP-712 type name from the module's last segment (MyApp.Messages.Mail -> "Mail"). typed_schema/2 takes an explicit type-name string.

The do block must contain one or more field/2,3 declarations, in order. The macro emits a defstruct and the __typed_data_schema__/0 introspection function.