X402.Extensions.EIP2612GasSponsoring (X402 v0.6.0)

Copy Markdown View Source

Builds, signs, and validates the eip2612GasSponsoring extension.

The extension enables a gasless approval flow for tokens that implement EIP-2612: instead of paying gas for an approve transaction, the client signs an off-chain EIP-2612 Permit authorizing the canonical Permit2 contract as spender, and the facilitator submits it on-chain (paying the gas) via x402Permit2Proxy.settleWithPermit.

Server side, build_extension/0 declares support under extensions.eip2612GasSponsoring in a PAYMENT-REQUIRED response, and extract_info/1 / validate_info/1 check the client-populated data echoed back in a PaymentPayload.

Client side, sign_permit/3 signs the Permit typed data for a selected payment requirements entry through the X402.Signer behaviour, and put_info/2 attaches the resulting info to a payload's extensions. enricher/2 packages both for X402.Client.build_payment/3:

{:ok, payload} =
  X402.Client.build_payment(payment_required, signer,
    extensions: [
      X402.Extensions.EIP2612GasSponsoring.enricher(signer, nonce: "0")
    ]
  )

The library has no chain access, so the owner's current EIP-2612 :nonce (read from the token contract's nonces(owner)) must be supplied by the caller.

See the eip2612GasSponsoring extension spec.

Summary

Types

Client-populated extension info in wire shape: string keys "from", "asset", "spender", "amount", "nonce", "deadline", "signature", and "version".

t()

A built extensions.eip2612GasSponsoring declaration (info + schema).

Functions

Builds the server-side extension declaration (info + schema).

Returns an enricher for X402.Client.build_payment/3's :extensions.

Extracts the client-populated info from a PaymentPayload map.

Returns the extension key, "eip2612GasSponsoring".

Returns the canonical Permit2 contract address — the default spender.

Computes the EIP-712 digest of an EIP-2612 Permit message.

Attaches client-populated info to a payload's extensions.

Returns the JSON Schema (Draft 2020-12) for the client-populated info.

Signs an EIP-2612 Permit for a payment requirements entry.

Validates the format of client-populated info.

Types

info()

@type info() :: %{optional(binary()) => binary()}

Client-populated extension info in wire shape: string keys "from", "asset", "spender", "amount", "nonce", "deadline", "signature", and "version".

info_error()

@type info_error() ::
  :extension_missing
  | {:missing_info_field, String.t()}
  | {:invalid_info_field, String.t()}

sign_error()

@type sign_error() ::
  :invalid_nonce
  | :invalid_deadline
  | :invalid_amount
  | :invalid_requirements
  | X402.EIP712.domain_error()
  | X402.EIP712.encode_error()
  | term()

t()

@type t() :: %{required(binary()) => map()}

A built extensions.eip2612GasSponsoring declaration (info + schema).

Functions

build_extension()

(since 0.6.0)
@spec build_extension() :: t()

Builds the server-side extension declaration (info + schema).

Resource servers advertise support by placing the declaration under extensions.eip2612GasSponsoring in a PAYMENT-REQUIRED response; the client populates the actual permit data.

Examples

iex> ext = X402.Extensions.EIP2612GasSponsoring.build_extension()
iex> ext["info"]["version"]
"1"
iex> ext["schema"]["required"]
["from", "asset", "spender", "amount", "nonce", "deadline", "signature", "version"]

enricher(signer, opts)

(since 0.6.0)
@spec enricher(
  X402.Signer.t(),
  keyword()
) :: (map(), map() | nil -> {:ok, map()} | {:error, sign_error()})

Returns an enricher for X402.Client.build_payment/3's :extensions.

The enricher signs an EIP-2612 permit for the payload's accepted requirements and attaches it via put_info/2 — but only when the server advertised eip2612GasSponsoring in the PaymentRequired extensions; otherwise the payload passes through unchanged.

Takes the same options as sign_permit/3 (:nonce is required).

extract_info(payload)

(since 0.6.0)
@spec extract_info(map()) :: {:ok, info()} | {:error, info_error()}

Extracts the client-populated info from a PaymentPayload map.

Returns the info when the extension is present and every required field is populated. Field formats are not checked here — see validate_info/1.

Examples

iex> X402.Extensions.EIP2612GasSponsoring.extract_info(%{"payload" => %{}})
{:error, :extension_missing}

key()

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

Returns the extension key, "eip2612GasSponsoring".

Examples

iex> X402.Extensions.EIP2612GasSponsoring.key()
"eip2612GasSponsoring"

permit2_address()

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

Returns the canonical Permit2 contract address — the default spender.

Examples

iex> X402.Extensions.EIP2612GasSponsoring.permit2_address()
"0x000000000022D473030F116dDEE9F6B43aC78BA3"

permit_digest(domain, permit)

(since 0.6.0)
@spec permit_digest(map(), map()) ::
  {:ok, <<_::256>>} | {:error, X402.EIP712.encode_error()}

Computes the EIP-712 digest of an EIP-2612 Permit message.

domain is the token's EIP-712 domain (see X402.EIP712.domain/1) and permit a map with "owner", "spender", "value", "nonce", and "deadline" keys (snake-case atom keys are also accepted). Useful for verifying a signed permit with X402.EIP3009.recover_signer/2.

put_info(payload, info)

(since 0.6.0)
@spec put_info(map(), info()) :: map()

Attaches client-populated info to a payload's extensions.

Places the info under extensions.eip2612GasSponsoring.info, following the append-only rule: when the payload already echoes the server's declaration, server-declared info fields are preserved (they win over client values) and the declared schema is kept; the client's fields are added alongside them.

Examples

iex> info = %{"from" => "0x1111111111111111111111111111111111111111"}
iex> payload = X402.Extensions.EIP2612GasSponsoring.put_info(%{"payload" => %{}}, info)
iex> payload["extensions"]["eip2612GasSponsoring"]["info"]["from"]
"0x1111111111111111111111111111111111111111"

schema()

(since 0.6.0)
@spec schema() :: map()

Returns the JSON Schema (Draft 2020-12) for the client-populated info.

sign_permit(requirements, signer, opts)

(since 0.6.0)
@spec sign_permit(map(), X402.Signer.t(), keyword()) ::
  {:ok, info()} | {:error, sign_error()}

Signs an EIP-2612 Permit for a payment requirements entry.

The EIP-712 domain is the token's: extra.name / extra.version, the chain id from the CAIP-2 network, and the asset contract as verifying contract. The permit authorizes :spender (canonical Permit2 by default) to spend :amount (the requirements' amount by default) of the owner's tokens until :deadline.

Returns the client-populated extension info in wire shape, ready for put_info/2.

Options

  • :nonce - Required. The owner's current EIP-2612 nonce on the token contract (the value of nonces(owner)), as an integer or decimal string. The library has no chain access, so the caller must read it.

  • :deadline - Unix timestamp at which the permit signature expires. Defaults to the current time plus the requirements' maxTimeoutSeconds.

  • :amount - Approval amount in atomic units. Defaults to the requirements' amountx402Permit2Proxy.settleWithPermit enforces that the permit value matches the Permit2 permitted amount exactly.

  • :spender (String.t/0) - The approved spender. Defaults to the canonical Permit2 contract.

Examples

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

{:ok, %{"signature" => _, "spender" => _} = info} =
  X402.Extensions.EIP2612GasSponsoring.sign_permit(
    %{
      "scheme" => "exact",
      "network" => "eip155:84532",
      "amount" => "10000",
      "asset" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
      "payTo" => "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
      "maxTimeoutSeconds" => 60,
      "extra" => %{"assetTransferMethod" => "permit2", "name" => "USDC", "version" => "2"}
    },
    signer,
    nonce: "0"
  )

validate_info(info)

(since 0.6.0)
@spec validate_info(term()) :: :ok | {:error, info_error()}

Validates the format of client-populated info.

Checks that addresses match ^0x[a-fA-F0-9]{40}$, that amount, nonce, and deadline are decimal strings, that signature is a 0x-prefixed hex string, and that version is a dotted numeric version.

Examples

iex> X402.Extensions.EIP2612GasSponsoring.validate_info(%{
...>   "from" => "0x857b06519E91e3A54538791bDbb0E22373e36b66",
...>   "asset" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
...>   "spender" => "0x000000000022D473030F116dDEE9F6B43aC78BA3",
...>   "amount" => "10000",
...>   "nonce" => "0",
...>   "deadline" => "1740672154",
...>   "signature" => "0xabcdef",
...>   "version" => "1"
...> })
:ok

iex> X402.Extensions.EIP2612GasSponsoring.validate_info(%{"from" => "0x123"})
{:error, {:invalid_info_field, "from"}}