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
endtyped_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 thatuses this DSL. The EIP-712 type string is that schema's declared type name, and it is registered (recursively) in the generatedtypesmap. - an atom (
:string,:address,:uint256) - an atomic Solidity type; the type string isAtom.to_string/1of it. - a string (
"uint256","bytes32") - a literal EIP-712 type string. {:array, inner}- a dynamic array ofinner("<inner>[]").{:array, inner, n}- a fixed-size array ofinner("<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
Declares a single field of the enclosing typed_schema.
key- the struct key (must be an atom). Declaring the samekeytwice 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 toto_string(key)).:default- thedefstructdefault for this key (defaults tonil).
@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.
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.