X402.EIP712 (X402 v0.6.0)

Copy Markdown View Source

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

domain()

@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.

domain_error()

@type domain_error() ::
  :invalid_requirements | {:missing_extra, String.t()} | :unsupported_network

encode_error()

@type encode_error() ::
  :missing_dependency
  | :invalid_address
  | :invalid_amount
  | :invalid_bytes32
  | :invalid_word
  | {:missing_field, String.t()}

Functions

chain_id_from_caip2(arg1)

(since 0.6.0)
@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}

digest(domain, struct_hash)

(since 0.6.0)
@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.

domain(requirements)

(since 0.6.0)
@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"}}

domain_separator(domain)

(since 0.6.0)
@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.

encode_address(address)

(since 0.6.0)
@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}

encode_bytes32(arg1)

(since 0.6.0)
@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}

encode_dynamic_bytes(bytes)

(since 0.6.0)
@spec encode_dynamic_bytes(binary()) :: binary()

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

encode_uint256(integer)

(since 0.6.0)
@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}

hash_struct(type, words)

(since 0.6.0)
@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.