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".
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
Client-populated extension info in wire shape: string keys "from",
"asset", "spender", "amount", "nonce", "deadline",
"signature", and "version".
@type sign_error() :: :invalid_nonce | :invalid_deadline | :invalid_amount | :invalid_requirements | X402.EIP712.domain_error() | X402.EIP712.encode_error() | term()
A built extensions.eip2612GasSponsoring declaration (info + schema).
Functions
@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"]
@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).
@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}
@spec key() :: String.t()
Returns the extension key, "eip2612GasSponsoring".
Examples
iex> X402.Extensions.EIP2612GasSponsoring.key()
"eip2612GasSponsoring"
@spec permit2_address() :: String.t()
Returns the canonical Permit2 contract address — the default spender.
Examples
iex> X402.Extensions.EIP2612GasSponsoring.permit2_address()
"0x000000000022D473030F116dDEE9F6B43aC78BA3"
@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.
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"
@spec schema() :: map()
Returns the JSON Schema (Draft 2020-12) for the client-populated info.
@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 ofnonces(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'amount—x402Permit2Proxy.settleWithPermitenforces 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"
)
@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"}}