X402.Extensions.ERC20ApprovalGasSponsoring (X402 v0.6.0)

Copy Markdown View Source

Builds and validates the erc20ApprovalGasSponsoring extension.

The extension enables a gasless Permit2 approval flow for ERC-20 tokens that do not implement EIP-2612. The client signs — but does not broadcast — a normal EVM transaction calling approve(Permit2, amount); the facilitator funds the wallet's gas if needed, broadcasts the approval, and settles via x402Permit2Proxy in one atomic bundle.

Unlike X402.Extensions.EIP2612GasSponsoring, nothing here is EIP-712 typed data: the signed artifact is a full RLP-encoded transaction whose nonce must match the wallet's current on-chain nonce and whose fees must match live network prices. Producing it therefore requires chain access and transaction-signing tooling outside this library — this module builds, validates, and attaches the extension data around a pre-signed transaction supplied by the caller.

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

Client side, build_info/1 assembles the wire info for a pre-signed approval transaction, put_info/2 attaches it to a payload's extensions, and enricher/1 packages both for X402.Client.build_payment/3:

{:ok, payload} =
  X402.Client.build_payment(payment_required, signer,
    extensions: [
      X402.Extensions.ERC20ApprovalGasSponsoring.enricher(
        from: wallet_address,
        signed_transaction: signed_approve_tx_hex
      )
    ]
  )

See the erc20ApprovalGasSponsoring extension spec.

Summary

Types

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

t()

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

Functions

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

Builds the client-populated info for a pre-signed approval transaction.

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

Extracts the client-populated info from a PaymentPayload map.

Returns the extension key, "erc20ApprovalGasSponsoring".

Returns MaxUint256 as a decimal string — the default approval amount.

Returns the canonical Permit2 contract address — the default spender.

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

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

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", "signedTransaction", and "version".

info_error()

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

t()

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

A built extensions.erc20ApprovalGasSponsoring 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.erc20ApprovalGasSponsoring in a PAYMENT-REQUIRED response; the client populates the actual approval data.

Examples

iex> ext = X402.Extensions.ERC20ApprovalGasSponsoring.build_extension()
iex> ext["info"]["version"]
"1"
iex> ext["schema"]["required"]
["from", "asset", "spender", "amount", "signedTransaction", "version"]

build_info(opts)

(since 0.6.0)
@spec build_info(keyword()) ::
  {:ok, info()} | {:error, {:invalid_info_field, String.t()}}

Builds the client-populated info for a pre-signed approval transaction.

Validates the declared fields structurally (the transaction itself is opaque to this library — the facilitator decodes and verifies it against the declared from, asset, spender, and amount) and returns the wire-shaped info, ready for put_info/2.

Options

  • :from (String.t/0) - Required. The address of the wallet that signed the approval transaction.

  • :signed_transaction (String.t/0) - Required. The RLP-encoded signed EIP-1559 transaction calling approve(spender, amount), as a 0x-prefixed hex string. Its signer, target contract, calldata, nonce, and fees are verified on-chain by the facilitator.

  • :amount - The approval amount declared alongside the transaction, in atomic units. Must match the amount in the transaction's calldata. Defaults to MaxUint256, matching the reference client implementations.

  • :spender (String.t/0) - The approved spender declared alongside the transaction. Must match the spender in the transaction's calldata. Defaults to the canonical Permit2 contract.

  • :asset (String.t/0) - Required. The ERC-20 token contract the transaction approves.

Examples

iex> {:ok, info} =
...>   X402.Extensions.ERC20ApprovalGasSponsoring.build_info(
...>     from: "0x857b06519E91e3A54538791bDbb0E22373e36b66",
...>     asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
...>     signed_transaction: "0x02f8" <> String.duplicate("ab", 100)
...>   )
iex> info["spender"]
"0x000000000022D473030F116dDEE9F6B43aC78BA3"
iex> info["version"]
"1"

iex> X402.Extensions.ERC20ApprovalGasSponsoring.build_info(
...>   from: "0x123",
...>   asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
...>   signed_transaction: "0xabcd"
...> )
{:error, {:invalid_info_field, "from"}}

enricher(opts)

(since 0.6.0)
@spec enricher(keyword()) :: (map(), map() | nil ->
                          {:ok, map()}
                          | {:error,
                             {:invalid_info_field, String.t()}
                             | :invalid_requirements})

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

The enricher attaches the pre-signed approval transaction via build_info/1 and put_info/2 — but only when the server advertised erc20ApprovalGasSponsoring in the PaymentRequired extensions; otherwise the payload passes through unchanged. :asset defaults to the accepted requirements' asset.

Options

  • :from (String.t/0) - Required. The address of the wallet that signed the approval transaction.

  • :signed_transaction (String.t/0) - Required. The RLP-encoded signed EIP-1559 transaction calling approve(spender, amount), as a 0x-prefixed hex string. Its signer, target contract, calldata, nonce, and fees are verified on-chain by the facilitator.

  • :amount - The approval amount declared alongside the transaction, in atomic units. Must match the amount in the transaction's calldata. Defaults to MaxUint256, matching the reference client implementations.

  • :spender (String.t/0) - The approved spender declared alongside the transaction. Must match the spender in the transaction's calldata. Defaults to the canonical Permit2 contract.

  • :asset (String.t/0) - The ERC-20 token contract the transaction approves. Defaults to the accepted payment requirements' asset.

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.ERC20ApprovalGasSponsoring.extract_info(%{"payload" => %{}})
{:error, :extension_missing}

key()

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

Returns the extension key, "erc20ApprovalGasSponsoring".

Examples

iex> X402.Extensions.ERC20ApprovalGasSponsoring.key()
"erc20ApprovalGasSponsoring"

max_uint256()

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

Returns MaxUint256 as a decimal string — the default approval amount.

Examples

iex> X402.Extensions.ERC20ApprovalGasSponsoring.max_uint256()
"115792089237316195423570985008687907853269984665640564039457584007913129639935"

permit2_address()

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

Returns the canonical Permit2 contract address — the default spender.

Examples

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

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.erc20ApprovalGasSponsoring.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.ERC20ApprovalGasSponsoring.put_info(%{"payload" => %{}}, info)
iex> payload["extensions"]["erc20ApprovalGasSponsoring"]["info"]["from"]
"0x1111111111111111111111111111111111111111"

schema()

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

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

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 is a decimal string, that signedTransaction is a 0x-prefixed hex string, and that version is a dotted numeric version.

Examples

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

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