X402.AuthCapture.EVM (X402 v0.9.0)

Copy Markdown View Source

EVM binding primitives for the auth-capture scheme: commerce-payments deployments, PaymentInfo hashing, authorizer typed data, and AuthCaptureEscrow calldata.

Everything here is pure: the module hashes, encodes, signs, and decodes but never talks to a node. X402.Verify.AuthCaptureEVM layers the RPC checks on top. These primitives do not submit transactions or provide durable lifecycle orchestration.

PaymentInfo

The escrow's struct keeps its canonical Solidity names on the wire so its EIP-712 typehash matches the contract byte for byte. Maps use string keys:

%{
  "operator" => "0x...",            # extra.captureAuthorizer
  "payer" => "0x...",               # the payload's `from`
  "receiver" => "0x...",            # requirements.payTo
  "token" => "0x...",               # requirements.asset
  "maxAmount" => "1000000",         # requirements.amount
  "preApprovalExpiry" => 1_740_675_754,
  "authorizationExpiry" => 1_740_758_554,
  "refundExpiry" => 1_741_276_954,
  "minFeeBps" => 100,
  "maxFeeBps" => 100,
  "feeReceiver" => "0x...",         # extra.feeRecipient
  "salt" => "0x..."                 # 32-byte hex, zero-padded
}

Integer fields accept integers or decimal strings; salt is always the 0x-prefixed 32-byte hex spelling the spec pins.

Payment identity

Two hashes derive from the struct and are not interchangeable:

  • signature_nonce/3 — keccak256(abi.encode(chainId, escrow, keccak256(abi.encode(TYPEHASH, info with payer = 0)))), the nonce inside the client's token authorization.
  • payment_info_hash/3 — AuthCaptureEscrow.getHash(info), the escrow's canonical identifier keyed over the real payer.

Requires the optional ex_keccak dependency (and ex_secp256k1 for signing and recovery).

Summary

Types

A resolved commerce-payments deployment.

Encoding failures shared by the hashing and calldata helpers.

An authorizer-signed operation.

A PaymentInfo struct in wire spelling (string keys).

The decoded paymentState(bytes32) tuple.

authorizer

Computes the EIP-712 digest an authorizer signs for an operation.

The EIP-712 type string an operation's consent is signed over.

The submitted fee field a deployment takes on charge and capture.

The EIP-712 domain every authorizer signature uses, with the capture authorizer as verifyingContract.

Recovers the ECDSA signer of an operation's consent signature.

Signs an operation's consent digest with the receiver authorizer's signer.

calldata

Encodes authorize(PaymentInfo, uint256 amount, address tokenCollector, bytes collectorData).

Encodes capture(PaymentInfo, uint256 amount, <fee>, address feeReceiver) (fee argument per charge_calldata/7).

Encodes charge(PaymentInfo, uint256 amount, address tokenCollector, bytes collectorData, <fee>, address feeReceiver).

Classifies a node revert onto the spec's typed simulation reverts.

Encodes the signature bytes for the selected token collector.

Returns the custom-error selector table (selector => {name, reason}).

Decodes the paymentState(bytes32) return data.

Returns the 0x topic of an escrow event.

Encodes paymentState(bytes32 paymentInfoHash).

Encodes reclaim(PaymentInfo) — the payer's own call after the capture deadline, never relayed by a facilitator.

Encodes refund(PaymentInfo, uint256 amount, address tokenCollector, bytes collectorData).

Returns the four-byte selector of an escrow function.

Encodes void(PaymentInfo).

client

Returns standard EIP-712 JSON for the client's token authorization.

Computes the EIP-712 digest of a witness-less Permit2 PermitTransferFrom.

Computes the EIP-712 digest of an EIP-3009 ReceiveWithAuthorization.

deployments

Whether salt binding is on: extra.receiverAuthorizer or extra.policy is a non-zero address.

Returns a canonical commerce-payments deployment by version.

Whether the value is a well-formed, non-zero EVM address.

Resolves the deployment a requirements entry (or its extra) selects.

The zero address, which the scheme reads for every absent operator field.

fees

Checks the submitted fee and fee receiver against the client-signed bounds, per the spec's fee system.

The escrow's fee arithmetic: amount * bps / 10000 with integer division.

payment_info

Computes the bound salt: keccak256(abi.encode(SALT_BINDING_TYPEHASH, receiverAuthorizer, policy, saltNonce)).

Spells a uint256 (integer or decimal string) as the zero-padded 32-byte hex the scheme pins for salt and saltNonce.

ABI-encodes a PaymentInfo struct into its twelve 32-byte words.

Checks stored PaymentInfo against its original requirements.

Parses a non-negative uint256 integer or decimal string.

Builds the PaymentInfo struct for a payment from its requirements.

Reconstructs the PaymentInfo a client payment payload commits to.

Computes AuthCaptureEscrow.getHash(paymentInfo) — the escrow's canonical payment identifier, as 0x-prefixed hex.

The PaymentInfo EIP-712 type string, whose keccak256 is the escrow's PAYMENT_INFO_TYPEHASH.

Spells a 32-byte hex value as the decimal uint256 string Permit2 nonces use on the wire.

Returns fresh random 32 bytes as zero-padded 0x hex — a salt when unbound, a saltNonce when bound.

The salt-binding type string (SALT_BINDING_TYPEHASH preimage).

Computes the payment's signatureNonce: the payer-agnostic identity that becomes the EIP-3009 nonce (as 0x hex) and the Permit2 nonce (as the decimal uint256, see permit2_nonce/1).

Types

deployment()

@type deployment() :: %{
  version: :v1_1 | :v1_0,
  escrow: String.t(),
  eip3009_collector: String.t(),
  permit2_collector: String.t(),
  refund_collector: String.t()
}

A resolved commerce-payments deployment.

encode_error()

@type encode_error() ::
  :missing_dependency
  | :invalid_address
  | :invalid_amount
  | :invalid_bytes32
  | :invalid_word
  | {:invalid_payment_info, String.t()}
  | {:missing_field, String.t()}

Encoding failures shared by the hashing and calldata helpers.

operation()

@type operation() :: :charge | :capture | :void | :refund

An authorizer-signed operation.

payment_info()

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

A PaymentInfo struct in wire spelling (string keys).

payment_state()

@type payment_state() :: %{
  collected?: boolean(),
  capturable_amount: non_neg_integer(),
  refundable_amount: non_neg_integer()
}

The decoded paymentState(bytes32) tuple.

authorizer

fee_field(atom)

(since 0.9.0)
@spec fee_field(:v1_1 | :v1_0) :: String.t()

The submitted fee field a deployment takes on charge and capture.

Examples

iex> X402.AuthCapture.EVM.fee_field(:v1_1)
"feeAmount"

iex> X402.AuthCapture.EVM.fee_field(:v1_0)
"feeBps"

operator_domain(chain_id, capture_authorizer)

(since 0.9.0)
@spec operator_domain(non_neg_integer(), String.t()) :: map()

The EIP-712 domain every authorizer signature uses, with the capture authorizer as verifyingContract.

Examples

iex> X402.AuthCapture.EVM.operator_domain(84532, "0x1563915e194d8cfba1943570603f7606a3115508")
%{
  name: "x402 Auth Capture Operator",
  version: "1",
  chain_id: 84532,
  verifying_contract: "0x1563915e194d8cfba1943570603f7606a3115508"
}

recover_consent(operation, params, domain, version, signature)

(since 0.9.0)
@spec recover_consent(operation(), map(), map(), :v1_1 | :v1_0, binary()) ::
  {:ok, String.t()} | {:error, term()}

Recovers the ECDSA signer of an operation's consent signature.

ERC-1271 authorizers need a node call and are handled by X402.Verify.AuthCaptureEVM.

sign_consent(signer, operation, params, domain, version)

(since 0.9.0)
@spec sign_consent(X402.Signer.t(), operation(), map(), map(), :v1_1 | :v1_0) ::
  {:ok, String.t()} | {:error, term()}

Signs an operation's consent digest with the receiver authorizer's signer.

Returns the 0x-prefixed 65-byte signature.

calldata

authorize_calldata(info, amount, collector, collector_data)

(since 0.9.0)
@spec authorize_calldata(map(), term(), String.t(), binary()) ::
  {:ok, binary()} | {:error, encode_error()}

Encodes authorize(PaymentInfo, uint256 amount, address tokenCollector, bytes collectorData).

capture_calldata(version, info, amount, fee, fee_receiver)

(since 0.9.0)
@spec capture_calldata(:v1_1 | :v1_0, map(), term(), term(), String.t()) ::
  {:ok, binary()} | {:error, encode_error()}

Encodes capture(PaymentInfo, uint256 amount, <fee>, address feeReceiver) (fee argument per charge_calldata/7).

charge_calldata(version, info, amount, collector, collector_data, fee, fee_receiver)

(since 0.9.0)
@spec charge_calldata(
  :v1_1 | :v1_0,
  map(),
  term(),
  String.t(),
  binary(),
  term(),
  String.t()
) ::
  {:ok, binary()} | {:error, encode_error()}

Encodes charge(PaymentInfo, uint256 amount, address tokenCollector, bytes collectorData, <fee>, address feeReceiver).

The fee argument is uint256 feeAmount on v1.1 and uint16 feeBps on v1.0 — the value passes through untouched, so pass the field the deployment takes.

classify_revert(error)

(since 0.9.0)
@spec classify_revert(map()) :: atom() | nil

Classifies a node revert onto the spec's typed simulation reverts.

Reads the custom-error selector from the error data when present and otherwise looks for the error name in the message. Returns nil for an unmapped revert.

Examples

iex> X402.AuthCapture.EVM.classify_revert(%{code: 3, message: "execution reverted", data: "0xad7c145a"})
:payment_already_collected

iex> X402.AuthCapture.EVM.classify_revert(%{code: 3, message: "reverted: AfterRefundExpiry(1, 2)", data: nil})
:refund_deadline_expired

iex> X402.AuthCapture.EVM.classify_revert(%{code: 3, message: "boom", data: nil})
nil

collector_data(atom, signature)

(since 0.9.0)
@spec collector_data(:eip3009 | :permit2, binary()) :: binary()

Encodes the signature bytes for the selected token collector.

EIP-3009 takes the original bytes; Permit2 takes abi.encode(bytes). An ERC-6492 wrapper remains intact inside those bytes.

Examples

iex> X402.AuthCapture.EVM.collector_data(:eip3009, <<1, 2>>)
<<1, 2>>

iex> byte_size(X402.AuthCapture.EVM.collector_data(:permit2, <<1, 2>>))
96

custom_errors()

(since 0.9.0)
@spec custom_errors() :: %{required(<<_::32>>) => {String.t(), atom()}}

Returns the custom-error selector table (selector => {name, reason}).

decode_payment_state(arg1)

(since 0.9.0)
@spec decode_payment_state(term()) ::
  {:ok, payment_state()} | {:error, :invalid_payment_state}

Decodes the paymentState(bytes32) return data.

Examples

iex> X402.AuthCapture.EVM.decode_payment_state(
...>   "0x" <> String.duplicate("00", 31) <> "01" <>
...>     String.duplicate("00", 30) <> "2710" <> String.duplicate("00", 32)
...> )
{:ok, %{collected?: true, capturable_amount: 10000, refundable_amount: 0}}

iex> X402.AuthCapture.EVM.decode_payment_state("0x")
{:error, :invalid_payment_state}

event_topic(name)

(since 0.9.0)
@spec event_topic(atom()) :: String.t()

Returns the 0x topic of an escrow event.

Examples

iex> X402.AuthCapture.EVM.event_topic(:voided)
"0xcadce8c3acb008e3e1c64ca7f60d22a3c87069183182b7dbb9e4d8cfb3a15842"

payment_state_calldata(payment_info_hash)

(since 0.9.0)
@spec payment_state_calldata(String.t()) ::
  {:ok, binary()} | {:error, :invalid_bytes32}

Encodes paymentState(bytes32 paymentInfoHash).

reclaim_calldata(info)

(since 0.9.0)
@spec reclaim_calldata(map()) :: {:ok, binary()} | {:error, encode_error()}

Encodes reclaim(PaymentInfo) — the payer's own call after the capture deadline, never relayed by a facilitator.

refund_calldata(info, amount, collector, collector_data \\ <<>>)

(since 0.9.0)
@spec refund_calldata(map(), term(), String.t(), binary()) ::
  {:ok, binary()} | {:error, encode_error()}

Encodes refund(PaymentInfo, uint256 amount, address tokenCollector, bytes collectorData).

Facilitator-relayed refunds use the deployment's operator refund collector with empty collectorData.

selector(name)

(since 0.9.0)
@spec selector(atom()) :: <<_::32>>

Returns the four-byte selector of an escrow function.

Examples

iex> X402.AuthCapture.EVM.selector(:void)
<<0xFA, 0x1C, 0xAD, 0x17>>

iex> X402.AuthCapture.EVM.selector(:payment_state)
<<0x34, 0xB7, 0x78, 0xED>>

void_calldata(info)

(since 0.9.0)
@spec void_calldata(map()) :: {:ok, binary()} | {:error, encode_error()}

Encodes void(PaymentInfo).

client

authorization_typed_data(atom, domain, authorization)

(since 0.9.0)
@spec authorization_typed_data(:eip3009 | :permit2, map(), map()) :: map()

Returns standard EIP-712 JSON for the client's token authorization.

Includes types, primaryType, a camel-case domain, and only the signed message fields. Permit2 has no domain version or signed from.

permit_transfer_digest(domain, authorization)

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

Computes the EIP-712 digest of a witness-less Permit2 PermitTransferFrom.

domain is the canonical Permit2 domain (see X402.Permit2.domain/1); the authorization carries permitted.token, permitted.amount, spender, nonce (decimal uint256), and deadline.

receive_authorization_digest(domain, authorization)

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

Computes the EIP-712 digest of an EIP-3009 ReceiveWithAuthorization.

Same field layout as TransferWithAuthorization, different type name — the token collector calls receiveWithAuthorization. domain is the token's EIP-712 domain (see X402.EIP712.domain/1).

deployments

bound?(requirements)

(since 0.9.0)
@spec bound?(map()) :: boolean()

Whether salt binding is on: extra.receiverAuthorizer or extra.policy is a non-zero address.

Examples

iex> X402.AuthCapture.EVM.bound?(%{"extra" => %{}})
false

iex> X402.AuthCapture.EVM.bound?(%{
...>   "extra" => %{"receiverAuthorizer" => "0x2222222222222222222222222222222222222222"}
...> })
true

deployment(atom)

(since 0.9.0)
@spec deployment(:v1_1 | :v1_0) :: deployment()

Returns a canonical commerce-payments deployment by version.

Examples

iex> X402.AuthCapture.EVM.deployment(:v1_1).escrow
"0xf96815976523E00e65Be8f34cA5e64b4f41EB19c"

iex> X402.AuthCapture.EVM.deployment(:v1_0).eip3009_collector
"0x0E3dF9510de65469C4518D7843919c0b8C7A7757"

nonzero_address?(address)

(since 0.9.0)
@spec nonzero_address?(term()) :: boolean()

Whether the value is a well-formed, non-zero EVM address.

Examples

iex> X402.AuthCapture.EVM.nonzero_address?("0x2222222222222222222222222222222222222222")
true

iex> X402.AuthCapture.EVM.nonzero_address?("0x0000000000000000000000000000000000000000")
false

iex> X402.AuthCapture.EVM.nonzero_address?(nil)
false

resolve_deployment(requirements)

(since 0.9.0)
@spec resolve_deployment(map()) :: {:ok, deployment()} | {:error, :invalid_escrow}

Resolves the deployment a requirements entry (or its extra) selects.

An absent extra.authCaptureEscrow is the v1.1 escrow; either canonical escrow address (case-insensitive) selects its deployment; any other value is {:error, :invalid_escrow}.

Examples

iex> {:ok, deployment} = X402.AuthCapture.EVM.resolve_deployment(%{"extra" => %{}})
iex> deployment.version
:v1_1

iex> {:ok, deployment} =
...>   X402.AuthCapture.EVM.resolve_deployment(%{
...>     "extra" => %{"authCaptureEscrow" => "0xbdea0d1bcc5966192b070fdf62ab4ef5b4420cff"}
...>   })
iex> deployment.version
:v1_0

iex> X402.AuthCapture.EVM.resolve_deployment(%{
...>   "extra" => %{"authCaptureEscrow" => "0x1111111111111111111111111111111111111111"}
...> })
{:error, :invalid_escrow}

zero_address()

(since 0.9.0)
@spec zero_address() :: String.t()

The zero address, which the scheme reads for every absent operator field.

Examples

iex> X402.AuthCapture.EVM.zero_address()
"0x0000000000000000000000000000000000000000"

fees

check_fee(version, amount, fee, submitted_receiver, min_bps, max_bps, signed_receiver)

(since 0.9.0)
@spec check_fee(
  :v1_1 | :v1_0,
  non_neg_integer(),
  non_neg_integer(),
  String.t() | nil,
  non_neg_integer(),
  non_neg_integer(),
  String.t()
) :: :ok | {:error, :fee_bps_out_of_range | :fee_receiver | :zero_fee_receiver}

Checks the submitted fee and fee receiver against the client-signed bounds, per the spec's fee system.

v1.1 requires amount * minFeeBps / 10000 <= feeAmount <= amount * maxFeeBps / 10000; v1.0 requires minFeeBps <= feeBps <= maxFeeBps. A non-zero PaymentInfo.feeReceiver must equal the submitted one; a zero one admits any non-zero address, and a zero submitted receiver with a non-zero fee reverts onchain.

Examples

iex> X402.AuthCapture.EVM.check_fee(:v1_1, 750_000, 7500, "0x2222222222222222222222222222222222222222", 100, 100, "0x2222222222222222222222222222222222222222")
:ok

iex> X402.AuthCapture.EVM.check_fee(:v1_1, 750_000, 7501, "0x2222222222222222222222222222222222222222", 100, 100, "0x2222222222222222222222222222222222222222")
{:error, :fee_bps_out_of_range}

iex> X402.AuthCapture.EVM.check_fee(:v1_0, 750_000, 50, "0x2222222222222222222222222222222222222222", 100, 100, "0x2222222222222222222222222222222222222222")
{:error, :fee_bps_out_of_range}

iex> X402.AuthCapture.EVM.check_fee(:v1_1, 750_000, 7500, "0x3333333333333333333333333333333333333333", 100, 100, "0x2222222222222222222222222222222222222222")
{:error, :fee_receiver}

fee_amount(amount, bps)

(since 0.9.0)
@spec fee_amount(non_neg_integer(), non_neg_integer()) :: non_neg_integer()

The escrow's fee arithmetic: amount * bps / 10000 with integer division.

Examples

iex> X402.AuthCapture.EVM.fee_amount(750_000, 100)
7500

iex> X402.AuthCapture.EVM.fee_amount(999, 100)
9

payment_info

bound_salt(receiver_authorizer, policy, salt_nonce)

(since 0.9.0)
@spec bound_salt(String.t() | nil, String.t() | nil, String.t()) ::
  {:ok, String.t()} | {:error, encode_error()}

Computes the bound salt: keccak256(abi.encode(SALT_BINDING_TYPEHASH, receiverAuthorizer, policy, saltNonce)).

Absent addresses are the zero address; salt_nonce is 32-byte hex.

bytes32_hex(value)

(since 0.9.0)
@spec bytes32_hex(term()) :: {:ok, String.t()} | {:error, :invalid_bytes32}

Spells a uint256 (integer or decimal string) as the zero-padded 32-byte hex the scheme pins for salt and saltNonce.

Examples

iex> X402.AuthCapture.EVM.bytes32_hex(255)
{:ok, "0x00000000000000000000000000000000000000000000000000000000000000ff"}

iex> X402.AuthCapture.EVM.bytes32_hex("0x" <> String.duplicate("ab", 32))
{:ok, "0x" <> String.duplicate("ab", 32)}

iex> X402.AuthCapture.EVM.bytes32_hex("nope")
{:error, :invalid_bytes32}

encode_payment_info(info)

(since 0.9.0)
@spec encode_payment_info(map()) ::
  {:ok, binary()} | {:error, {:invalid_payment_info, String.t()}}

ABI-encodes a PaymentInfo struct into its twelve 32-byte words.

Validates each field against its Solidity width (uint120, uint48, uint16) and returns {:error, {:invalid_payment_info, field}} for the first field that does not fit.

Examples

iex> info = %{
...>   "operator" => "0x1563915e194d8cfba1943570603f7606a3115508",
...>   "payer" => "0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
...>   "receiver" => "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
...>   "token" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
...>   "maxAmount" => "10000",
...>   "preApprovalExpiry" => 1,
...>   "authorizationExpiry" => 2,
...>   "refundExpiry" => 3,
...>   "minFeeBps" => 0,
...>   "maxFeeBps" => 100,
...>   "feeReceiver" => "0x0000000000000000000000000000000000000000",
...>   "salt" => "0x" <> String.duplicate("00", 31) <> "01"
...> }
iex> {:ok, encoded} = X402.AuthCapture.EVM.encode_payment_info(info)
iex> byte_size(encoded)
384

iex> X402.AuthCapture.EVM.encode_payment_info(%{"operator" => "0x1"})
{:error, {:invalid_payment_info, "operator"}}

match_payment_info(requirements, info)

(since 0.9.0)
@spec match_payment_info(map(), map()) :: :ok | {:error, term()}

Checks stored PaymentInfo against its original requirements.

Reconstructs and compares the canonical ABI encoding, including every Solidity width, and checks expiry ordering. The payer, preapproval expiry and salt must come from retained client state; this does not verify their signature, salt commitment, or onchain existence.

Examples

iex> X402.AuthCapture.EVM.match_payment_info(%{}, %{})
{:error, {:missing_field, "captureAuthorizer"}}

parse_uint256(value)

(since 0.9.0)
@spec parse_uint256(term()) :: {:ok, non_neg_integer()} | {:error, :invalid_amount}

Parses a non-negative uint256 integer or decimal string.

Rejects signs, whitespace and strings longer than 78 digits before integer conversion. Hex input is reserved for the bytes32 helpers.

Examples

iex> X402.AuthCapture.EVM.parse_uint256("1000")
{:ok, 1000}

iex> X402.AuthCapture.EVM.parse_uint256("+1")
{:error, :invalid_amount}

payment_info(requirements, payer, pre_approval_expiry, salt)

(since 0.9.0)
@spec payment_info(map(), String.t(), non_neg_integer(), String.t()) ::
  {:ok, payment_info()} | {:error, {:missing_field, String.t()}}

Builds the PaymentInfo struct for a payment from its requirements.

payer is the client address, pre_approval_expiry the authorization's validBefore / deadline, and salt the wire payload.salt. Every other field comes from requirements and its extra, as the spec's PaymentInfo appendix sets out.

Examples

iex> requirements = %{
...>   "amount" => "10000",
...>   "asset" => "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
...>   "payTo" => "0x209693Bc6afc0C5328bA36FaF03C514EF312287C",
...>   "extra" => %{
...>     "captureAuthorizer" => "0x1563915e194d8cfba1943570603f7606a3115508",
...>     "feeRecipient" => "0x0000000000000000000000000000000000000000",
...>     "captureDeadline" => 1_800_000_000,
...>     "refundDeadline" => 1_800_100_000,
...>     "minFeeBps" => 0,
...>     "maxFeeBps" => 0
...>   }
...> }
iex> {:ok, info} =
...>   X402.AuthCapture.EVM.payment_info(
...>     requirements,
...>     "0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
...>     1_799_000_000,
...>     "0x" <> String.duplicate("ab", 32)
...>   )
iex> {info["operator"], info["maxAmount"], info["authorizationExpiry"]}
{"0x1563915e194d8cfba1943570603f7606a3115508", "10000", 1800000000}

iex> X402.AuthCapture.EVM.payment_info(%{"extra" => %{}}, "0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a", 1, "0x00")
{:error, {:missing_field, "captureAuthorizer"}}

payment_info_from_payload(payload, requirements)

(since 0.9.0)
@spec payment_info_from_payload(map(), map()) ::
  {:ok, payment_info()}
  | {:error, :payload_format | {:missing_field, String.t()}}

Reconstructs the PaymentInfo a client payment payload commits to.

Accepts the full v2 envelope or the inner payload map. The payer and preApprovalExpiry come from the EIP-3009 authorization (from, validBefore) or the permit2Authorization (from, deadline), and the salt from payload.salt.

payment_info_hash(chain_id, escrow, info)

(since 0.9.0)
@spec payment_info_hash(non_neg_integer(), String.t(), map()) ::
  {:ok, String.t()} | {:error, encode_error()}

Computes AuthCaptureEscrow.getHash(paymentInfo) — the escrow's canonical payment identifier, as 0x-prefixed hex.

keccak256(abi.encode(chainId, escrow, keccak256(abi.encode(PAYMENT_INFO_TYPEHASH, info)))).

payment_info_type()

(since 0.9.0)
@spec payment_info_type() :: String.t()

The PaymentInfo EIP-712 type string, whose keccak256 is the escrow's PAYMENT_INFO_TYPEHASH.

permit2_nonce(hex)

(since 0.9.0)
@spec permit2_nonce(String.t()) :: {:ok, String.t()} | {:error, :invalid_bytes32}

Spells a 32-byte hex value as the decimal uint256 string Permit2 nonces use on the wire.

Examples

iex> X402.AuthCapture.EVM.permit2_nonce("0x" <> String.duplicate("00", 31) <> "ff")
{:ok, "255"}

iex> X402.AuthCapture.EVM.permit2_nonce("0xzz")
{:error, :invalid_bytes32}

random_bytes32()

(since 0.9.0)
@spec random_bytes32() :: String.t()

Returns fresh random 32 bytes as zero-padded 0x hex — a salt when unbound, a saltNonce when bound.

Examples

iex> X402.AuthCapture.EVM.random_bytes32() =~ ~r/^0x[0-9a-f]{64}$/
true

salt_binding_type()

(since 0.9.0)
@spec salt_binding_type() :: String.t()

The salt-binding type string (SALT_BINDING_TYPEHASH preimage).

signature_nonce(chain_id, escrow, info)

(since 0.9.0)
@spec signature_nonce(non_neg_integer(), String.t(), map()) ::
  {:ok, String.t()} | {:error, encode_error()}

Computes the payment's signatureNonce: the payer-agnostic identity that becomes the EIP-3009 nonce (as 0x hex) and the Permit2 nonce (as the decimal uint256, see permit2_nonce/1).

The struct is hashed with payer zeroed and every other field holding its onchain value.