X402.Permit2 (X402 v0.6.0)

Copy Markdown View Source

Permit2 PermitWitnessTransferFrom building and EIP-712 signing for the x402 upto scheme on EVM networks.

Implements the client half of the upto scheme: building a Permit2 authorization from v2 payment requirements, computing its EIP-712 digest against the canonical Permit2 domain, and signing it through the X402.Signer behaviour to produce the scheme payload map

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

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

The upto authorization

Per the upto-EVM scheme specification, the client signs a PermitWitnessTransferFrom message for the canonical Permit2 contract (0x000000000022D473030F116dDEE9F6B43aC78BA3) where

  • permitted.token / permitted.amount are the requirements' asset and amount — the amount is the maximum the client authorizes; the server settles for the actual usage, up to this ceiling;
  • spender is the x402UptoPermit2Proxy contract (0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002), deployed at the same address on every supported EVM chain;
  • the witness struct Witness(address to,address facilitator,uint256 validAfter) binds the requirements' payTo (to) and the facilitator announced in extra.facilitatorAddress (facilitator), so no other party can settle the authorization. Facilitators announce their address via GET /supported (X402.Facilitator.supported/1), and resource servers forward it inside each upto requirements entry's extra.

The EIP-712 domain is the canonical Permit2 domain — name "Permit2", the chain id from the CAIP-2 network, the Permit2 contract as the verifying contract, and no version field.

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

Types

A Permit2 PermitWitnessTransferFrom authorization in wire shape.

The scheme payload map for a signed upto Permit2 payment.

Functions

Builds an upto PermitWitnessTransferFrom authorization in wire shape.

Fetches the facilitator address from the requirements' extra.

Returns the canonical Permit2 contract address.

Returns a fresh random uint256 nonce as a decimal string.

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

Signs the upto Permit2 scheme payload for the given requirements.

Computes the EIP-712 digest of an upto PermitWitnessTransferFrom.

Derives the canonical Permit2 EIP-712 domain from v2 payment requirements.

Returns the x402UptoPermit2Proxy contract address — the upto spender.

Types

authorization()

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

A Permit2 PermitWitnessTransferFrom authorization in wire shape.

String keys "from", "permitted" (%{"token", "amount"}), "spender", "nonce", "deadline", and "witness" (%{"to", "facilitator", "validAfter"}).

authorization_error()

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

domain_error()

@type domain_error() :: :invalid_requirements | :unsupported_network

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 upto Permit2 payment.

Functions

build_upto_authorization(requirements, from)

(since 0.6.0)
@spec build_upto_authorization(map(), String.t()) ::
  {:ok, authorization()} | {:error, authorization_error()}

Builds an upto PermitWitnessTransferFrom authorization in wire shape.

permitted carries the requirements' asset and maximum amount; the spender is the x402UptoPermit2Proxy; the nonce is a fresh random uint256; the deadline is now plus maxTimeoutSeconds; and the witness binds payTo (to), extra.facilitatorAddress (facilitator), and validAfter "0" (immediately valid), mirroring the reference SDKs.

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

facilitator_address(requirements)

(since 0.6.0)
@spec facilitator_address(map()) ::
  {:ok, String.t()} | {:error, {:missing_extra, String.t()}}

Fetches the facilitator address from the requirements' extra.

The upto scheme requires extra.facilitatorAddress — the facilitator announces it via GET /supported (X402.Facilitator.supported/1) and the resource server forwards it in each upto requirements entry; the client binds it into the signed witness.

Examples

iex> X402.Permit2.facilitator_address(%{
...>   "extra" => %{"facilitatorAddress" => "0x2222222222222222222222222222222222222222"}
...> })
{:ok, "0x2222222222222222222222222222222222222222"}

iex> X402.Permit2.facilitator_address(%{"extra" => %{}})
{:error, {:missing_extra, "facilitatorAddress"}}

permit2_address()

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

Returns the canonical Permit2 contract address.

Examples

iex> X402.Permit2.permit2_address()
"0x000000000022D473030F116dDEE9F6B43aC78BA3"

random_nonce()

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

Returns a fresh random uint256 nonce as a decimal string.

Permit2 uses unordered nonces; the reference SDKs draw 32 random bytes per authorization, making collisions negligible.

Examples

iex> nonce = X402.Permit2.random_nonce()
iex> String.match?(nonce, ~r/^[0-9]+$/)
true

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.

sign_upto(requirements, signer)

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

Signs the upto Permit2 scheme payload for the given requirements.

Derives the canonical Permit2 domain from the requirements' network, builds a fresh authorization from the signer's address (from), the requirements' asset/amount (permitted), payTo and extra.facilitatorAddress (the witness), and maxTimeoutSeconds (the deadline), computes the EIP-712 digest, and signs it through signer.

Requirements without extra.facilitatorAddress return {:error, {:missing_extra, "facilitatorAddress"}} — the facilitator address is required so the witness can bind settlement to it.

Examples

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

{:ok, %{"signature" => _, "permit2Authorization" => _}} =
  X402.Permit2.sign_upto(
    %{
      "scheme" => "upto",
      "network" => "eip155:84532",
      "amount" => "5000000",
      "asset" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
      "payTo" => "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
      "maxTimeoutSeconds" => 300,
      "extra" => %{
        "name" => "USDC",
        "version" => "2",
        "facilitatorAddress" => "0x2222222222222222222222222222222222222222"
      }
    },
    signer
  )

upto_digest(domain, authorization)

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

Computes the EIP-712 digest of an upto PermitWitnessTransferFrom.

Returns keccak256(0x19 0x01 || domainSeparator || structHash) as a 32-byte binary — the value the client signs and the value to recover the payer address from (X402.EIP3009.recover_signer/2). domain and authorization accept both the internal snake-case atom keys and the wire-style string keys; the authorization's from is not part of the signed struct (Permit2 recovers the owner from the signature).

upto_domain(requirements)

(since 0.6.0)
@spec upto_domain(map()) :: {:ok, X402.EIP712.domain()} | {:error, domain_error()}

Derives the canonical Permit2 EIP-712 domain from v2 payment requirements.

The domain is name: "Permit2", the chain id from the CAIP-2 network, and the canonical Permit2 contract as the verifying contract. Permit2 declares no domain version, so the returned map carries no :version key and X402.EIP712.domain_separator/1 hashes the three-field EIP712Domain type.

Examples

iex> X402.Permit2.upto_domain(%{"network" => "eip155:84532"})
{:ok,
 %{
   name: "Permit2",
   chain_id: 84532,
   verifying_contract: "0x000000000022D473030F116dDEE9F6B43aC78BA3"
 }}

iex> X402.Permit2.upto_domain(%{"network" => "solana:mainnet"})
{:error, :unsupported_network}

upto_proxy_address()

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

Returns the x402UptoPermit2Proxy contract address — the upto spender.

Deployed at the same address on every supported EVM chain via CREATE2.

Examples

iex> X402.Permit2.upto_proxy_address()
"0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002"