Generic EIP-712 hashing primitives.
Shared by the x402 signing modules: X402.EIP3009 hashes
TransferWithAuthorization structs with these primitives,
X402.Permit2 hashes Permit2 PermitWitnessTransferFrom structs, and
X402.Extensions.EIP2612GasSponsoring hashes EIP-2612 Permit structs.
The module covers the common x402 domain shape
(name/version/chainId/verifyingContract, the version optional
for version-less domains such as Permit2's) and the generic
hash_struct/2 / digest/2 combination
digest = keccak256(0x19 0x01 || domainSeparator || structHash)together with the ABI word encoders the struct hashes are built from.
Cryptographic hashing requires the optional ex_keccak dependency and
returns {:error, :missing_dependency} when it is unavailable; the library
itself compiles without it.
Summary
Types
An EIP-712 domain map with :name, :version, :chain_id, and
:verifying_contract keys.
Functions
Extracts the chain id from an eip155:<chainId> CAIP-2 network identifier.
Computes the final EIP-712 digest for a domain and a struct hash.
Derives the EIP-712 domain from v2 payment requirements.
Computes the EIP-712 domain separator for a domain map.
ABI-encodes a 0x-prefixed EVM address into a 32-byte word.
Decodes a 0x-prefixed hex string into a 32-byte binary.
ABI-encodes a dynamic bytes value: a 32-byte length word followed by the
bytes right-padded to a 32-byte boundary.
ABI-encodes a non-negative integer (or decimal string) into a 32-byte word.
Computes an EIP-712 struct hash from a type string and encoded words.
Types
@type domain() :: %{ optional(:version) => String.t(), name: String.t(), chain_id: non_neg_integer(), verifying_contract: String.t() }
An EIP-712 domain map with :name, :version, :chain_id, and
:verifying_contract keys.
Functions accepting a domain also take the wire-style keys
("name", "version", "chainId", "verifyingContract"). Domains
whose contracts declare no version (for example the canonical Permit2
contract) omit the version key entirely; domain_separator/1 then hashes
the three-field EIP712Domain type.
@type domain_error() :: :invalid_requirements | {:missing_extra, String.t()} | :unsupported_network
@type encode_error() :: :missing_dependency | :invalid_address | :invalid_amount | :invalid_bytes32 | :invalid_word | {:missing_field, String.t()}
Functions
@spec chain_id_from_caip2(term()) :: {:ok, non_neg_integer()} | {:error, :unsupported_network}
Extracts the chain id from an eip155:<chainId> CAIP-2 network identifier.
Examples
iex> X402.EIP712.chain_id_from_caip2("eip155:84532")
{:ok, 84532}
iex> X402.EIP712.chain_id_from_caip2("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp")
{:error, :unsupported_network}
@spec digest(map(), <<_::256>>) :: {:ok, <<_::256>>} | {:error, encode_error()}
Computes the final EIP-712 digest for a domain and a struct hash.
Returns keccak256(0x19 0x01 || domainSeparator || structHash) as a
32-byte binary — the value a signer signs.
@spec domain(map()) :: {:ok, domain()} | {:error, domain_error()}
Derives the EIP-712 domain from v2 payment requirements.
Per the exact-EVM scheme specification, extra.name and extra.version
are required, the chain id comes from the CAIP-2 network, and the
verifying contract is the asset address. Unlike X402.EIP3009.domain/1,
the extra.assetTransferMethod is not restricted, so the same derivation
serves EIP-3009, Permit2, and extension signing.
Examples
iex> X402.EIP712.domain(%{
...> "network" => "eip155:84532",
...> "asset" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
...> "extra" => %{"name" => "USDC", "version" => "2"}
...> })
{:ok,
%{
name: "USDC",
version: "2",
chain_id: 84532,
verifying_contract: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
}}
iex> X402.EIP712.domain(%{"network" => "eip155:84532", "asset" => "0xasset", "extra" => %{}})
{:error, {:missing_extra, "name"}}
@spec domain_separator(map()) :: {:ok, <<_::256>>} | {:error, encode_error()}
Computes the EIP-712 domain separator for a domain map.
Returns keccak256(typeHash || keccak256(name) || keccak256(version) || chainId || verifyingContract) as a 32-byte binary. When the domain has
no version (absent or nil — for example the canonical Permit2
contract), the version-less three-field EIP712Domain type is hashed
instead, per EIP-712's rule that absent fields are dropped from the
domain type.
@spec encode_address(term()) :: {:ok, <<_::256>>} | {:error, :invalid_address}
ABI-encodes a 0x-prefixed EVM address into a 32-byte word.
Examples
iex> {:ok, word} = X402.EIP712.encode_address("0x1111111111111111111111111111111111111111")
iex> byte_size(word)
32
iex> X402.EIP712.encode_address("0x123")
{:error, :invalid_address}
@spec encode_bytes32(term()) :: {:ok, <<_::256>>} | {:error, :invalid_bytes32}
Decodes a 0x-prefixed hex string into a 32-byte binary.
Examples
iex> {:ok, bytes} = X402.EIP712.encode_bytes32("0x" <> String.duplicate("ab", 32))
iex> byte_size(bytes)
32
iex> X402.EIP712.encode_bytes32("0xdead")
{:error, :invalid_bytes32}
ABI-encodes a dynamic bytes value: a 32-byte length word followed by the
bytes right-padded to a 32-byte boundary.
Used to build calldata for functions taking bytes arguments
(isValidSignature, the bytes-signature transferWithAuthorization
variant, aggregate3).
Examples
iex> X402.EIP712.encode_dynamic_bytes(<<0xAB>>)
<<1::unsigned-big-integer-size(256), 0xAB, 0::unsigned-big-integer-size(248)>>
@spec encode_uint256(term()) :: {:ok, <<_::256>>} | {:error, :invalid_amount}
ABI-encodes a non-negative integer (or decimal string) into a 32-byte word.
Examples
iex> X402.EIP712.encode_uint256(1)
{:ok, <<1::unsigned-big-integer-size(256)>>}
iex> X402.EIP712.encode_uint256("not a number")
{:error, :invalid_amount}
@spec hash_struct(String.t(), [<<_::256>>]) :: {:ok, <<_::256>>} | {:error, :missing_dependency | :invalid_word}
Computes an EIP-712 struct hash from a type string and encoded words.
type is the full encoded type (for example
"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)")
and words the ABI-encoded 32-byte field values, in declaration order —
see encode_address/1, encode_uint256/1, and encode_bytes32/1.