X402.EIP3009 (X402 v0.6.0)

Copy Markdown View Source

EIP-3009 TransferWithAuthorization building and EIP-712 signing.

Implements the client half of the x402 exact scheme on EVM networks with the default eip3009 asset transfer method: building an authorization from v2 payment requirements, computing its EIP-712 digest, and signing it through the X402.Signer behaviour to produce the scheme payload map

%{"signature" => "0x...", "authorization" => %{...}}

carried inside a v2 PaymentPayload (see X402.Client.build_payment/3 for the full envelope).

The EIP-712 domain is derived from the payment requirements as specified by the exact-EVM scheme: name/version from extra, the chain id from the CAIP-2 network, and the verifying contract from asset.

Cryptographic operations require the optional ex_secp256k1 and ex_keccak dependencies and return {:error, :missing_dependency} when they are unavailable; the library itself compiles without them.

Summary

Payment Settlement

Builds the transferWithAuthorization calldata for an authorization and its raw signature bytes.

Types

A TransferWithAuthorization authorization in wire shape: string keys "from", "to", "value", "validAfter", "validBefore", "nonce".

An EIP-712 domain map — see X402.EIP712.domain/0.

The scheme payload map for a signed EIP-3009 payment.

Functions

Builds a TransferWithAuthorization authorization map in wire shape.

Extracts the chain id from an eip155:<chainId> CAIP-2 network identifier.

Derives the lowercase EVM address for a 32-byte secp256k1 private key.

Derives the EIP-712 domain from v2 payment requirements.

Computes the EIP-712 digest of a TransferWithAuthorization.

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 non-negative integer (or decimal string) into a 32-byte word.

Converts a 65-byte uncompressed (or 64-byte) secp256k1 public key to a lowercase 0x-prefixed EVM address.

Returns a fresh random 32-byte nonce as a 0x-prefixed hex string.

Recovers the signer address from an EIP-712 digest and a 65-byte signature.

Signs the exact/eip3009 scheme payload for the given requirements.

Signs an authorization's EIP-712 digest with a signer.

Payment Settlement

transfer_calldata(authorization, signature, signature_type)

(since 0.6.0)
@spec transfer_calldata(map(), binary(), :eoa | :erc1271 | :erc6492_counterfactual) ::
  {:ok, binary()} | {:error, encode_error() | :invalid_signature}

Builds the transferWithAuthorization calldata for an authorization and its raw signature bytes.

The overload is selected by the VERIFIED signature type, never by byte length: only :eoa signatures take the (v, r, s) variant (selector 0xe3ee160e, with v normalized to 27/28 as EIP-3009 contracts expect); contract signatures (:erc1271, :erc6492_counterfactual) — which can also be exactly 65 bytes — always take the dynamic-bytes variant (selector 0xcf092995), whose token-side SignatureChecker routes by account code. Shared by X402.Verify.EVM's eth_call simulation and X402.Facilitator.Engine's settlement transaction — the calldata a facilitator signs is always built here, from the verified authorization fields, and nowhere else.

Examples

iex> authorization = %{
...>   "from" => "0x1111111111111111111111111111111111111111",
...>   "to" => "0x2222222222222222222222222222222222222222",
...>   "value" => "10000",
...>   "validAfter" => "0",
...>   "validBefore" => "99999999999",
...>   "nonce" => "0x" <> String.duplicate("ab", 32)
...> }
iex> {:ok, calldata} = X402.EIP3009.transfer_calldata(authorization, <<1::520>>, :eoa)
iex> {binary_part(calldata, 0, 4), byte_size(calldata)}
{<<0xE3, 0xEE, 0x16, 0x0E>>, 4 + 9 * 32}
iex> {:ok, contract} = X402.EIP3009.transfer_calldata(authorization, <<1::520>>, :erc1271)
iex> binary_part(contract, 0, 4)
<<0xCF, 0x09, 0x29, 0x95>>

iex> X402.EIP3009.transfer_calldata(%{}, <<1::520>>, :eoa)
{:error, {:missing_field, "from"}}

Types

authorization()

@type authorization() :: %{optional(String.t()) => String.t()}

A TransferWithAuthorization authorization in wire shape: string keys "from", "to", "value", "validAfter", "validBefore", "nonce".

domain()

@type domain() :: X402.EIP712.domain()

An EIP-712 domain map — see X402.EIP712.domain/0.

domain_error()

@type domain_error() ::
  X402.EIP712.domain_error() | {:unsupported_transfer_method, term()}

encode_error()

@type encode_error() :: X402.EIP712.encode_error()

payload()

@type payload() :: %{optional(String.t()) => String.t() | authorization()}

The scheme payload map for a signed EIP-3009 payment.

Functions

build_authorization(requirements, from, opts \\ [])

(since 0.6.0)
@spec build_authorization(map(), String.t(), keyword()) ::
  {:ok, authorization()} | {:error, :invalid_requirements}

Builds a TransferWithAuthorization authorization map in wire shape.

value and to come from the requirements' amount and payTo; validAfter is now - valid_after_buffer, validBefore is now + maxTimeoutSeconds, and nonce is a fresh random 32-byte value.

Field values are validated when the digest is computed, not here.

Options

  • :valid_after_buffer (non_neg_integer/0) - Seconds subtracted from the current time for the authorization's validAfter, tolerating clock skew between payer, facilitator, and chain. The default value is 60.

chain_id_from_caip2(network)

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

See X402.EIP712.chain_id_from_caip2/1.

Examples

iex> X402.EIP3009.chain_id_from_caip2("eip155:84532")
{:ok, 84532}

iex> X402.EIP3009.chain_id_from_caip2("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp")
{:error, :unsupported_network}

derive_address(private_key)

(since 0.6.0)
@spec derive_address(binary()) ::
  {:ok, String.t()} | {:error, :missing_dependency | :invalid_private_key}

Derives the lowercase EVM address for a 32-byte secp256k1 private key.

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. Requirements selecting a non-default extra.assetTransferMethod are rejected.

Examples

iex> X402.EIP3009.domain(%{
...>   "network" => "eip155:84532",
...>   "asset" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
...>   "extra" => %{"name" => "USDC", "version" => "2"}
...> })
{:ok,
 %{
   name: "USDC",
   version: "2",
   chain_id: 84532,
   verifying_contract: "0x036CbD53842c5426634e7929541eC2318f3dCF7e"
 }}

iex> X402.EIP3009.domain(%{"network" => "eip155:84532", "asset" => "0xasset", "extra" => %{}})
{:error, {:missing_extra, "name"}}

eip712_digest(domain, authorization)

(since 0.6.0)
@spec eip712_digest(map(), map()) :: {:ok, <<_::256>>} | {:error, encode_error()}

Computes the EIP-712 digest of a TransferWithAuthorization.

Returns keccak256(0x19 0x01 || domainSeparator || structHash) as a 32-byte binary. domain and authorization accept both the internal snake-case atom keys and the wire-style string keys.

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.

See X402.EIP712.encode_address/1.

Examples

iex> {:ok, word} = X402.EIP3009.encode_address("0x1111111111111111111111111111111111111111")
iex> byte_size(word)
32

iex> X402.EIP3009.encode_address("0x123")
{:error, :invalid_address}

encode_bytes32(value)

(since 0.6.0)
@spec encode_bytes32(term()) :: {:ok, <<_::256>>} | {:error, :invalid_bytes32}

Decodes a 0x-prefixed hex string into a 32-byte binary.

See X402.EIP712.encode_bytes32/1.

Examples

iex> {:ok, bytes} = X402.EIP3009.encode_bytes32("0x" <> String.duplicate("ab", 32))
iex> byte_size(bytes)
32

iex> X402.EIP3009.encode_bytes32("0xdead")
{:error, :invalid_bytes32}

encode_uint256(value)

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

See X402.EIP712.encode_uint256/1.

Examples

iex> X402.EIP3009.encode_uint256(1)
{:ok, <<1::unsigned-big-integer-size(256)>>}

iex> X402.EIP3009.encode_uint256("not a number")
{:error, :invalid_amount}

public_key_to_address(public_key)

(since 0.6.0)
@spec public_key_to_address(binary()) ::
  {:ok, String.t()} | {:error, :missing_dependency | :invalid_public_key}

Converts a 65-byte uncompressed (or 64-byte) secp256k1 public key to a lowercase 0x-prefixed EVM address.

random_nonce()

(since 0.6.0)
@spec random_nonce() :: String.t()

Returns a fresh random 32-byte nonce as a 0x-prefixed hex string.

Examples

iex> nonce = X402.EIP3009.random_nonce()
iex> String.match?(nonce, ~r/^0x[0-9a-f]{64}$/)
true

recover_signer(digest, signature)

(since 0.6.0)
@spec recover_signer(binary(), binary()) ::
  {:ok, String.t()}
  | {:error, :missing_dependency | :invalid_signature | term()}

Recovers the signer address from an EIP-712 digest and a 65-byte signature.

Accepts the 0x-prefixed hex signature produced by sign_authorization/3 or the raw 65-byte binary. Useful for verifying a signed payment locally.

sign(requirements, signer, opts \\ [])

(since 0.6.0)
@spec sign(map(), X402.Signer.t(), keyword()) ::
  {:ok, payload()} | {:error, domain_error() | encode_error() | term()}

Signs the exact/eip3009 scheme payload for the given requirements.

Derives the EIP-712 domain from the requirements, builds a fresh authorization from the signer's address (from), payTo (to), and amount (value) with a random 32-byte nonce, computes the EIP-712 digest, and signs it through signer.

Options

  • :valid_after_buffer (non_neg_integer/0) - Seconds subtracted from the current time for the authorization's validAfter, tolerating clock skew between payer, facilitator, and chain. The default value is 60.

Examples

{:ok, signer} = X402.Signer.LocalKey.new(private_key)

{:ok, %{"signature" => _, "authorization" => _}} =
  X402.EIP3009.sign(
    %{
      "scheme" => "exact",
      "network" => "eip155:84532",
      "amount" => "10000",
      "asset" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
      "payTo" => "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
      "maxTimeoutSeconds" => 60,
      "extra" => %{"name" => "USDC", "version" => "2"}
    },
    signer
  )

sign_authorization(signer, domain, authorization)

(since 0.6.0)
@spec sign_authorization(X402.Signer.t(), map(), map()) ::
  {:ok, String.t()} | {:error, encode_error() | term()}

Signs an authorization's EIP-712 digest with a signer.

Returns the 0x-prefixed 65-byte r || s || v signature.