X402.Transaction (X402 v0.6.0)

Copy Markdown View Source

Minimal EIP-1559 (type-2) transaction encoding and signing digests.

Built for X402.Facilitator.Engine's settlement path: the facilitator assembles a transferWithAuthorization call, signs its digest through the X402.Signer behaviour, and broadcasts the raw transaction via eth_sendRawTransaction. The module is deliberately narrow — type-2 transactions only, always with an empty access list — and pure except for digest/1, which needs the optional ex_keccak dependency.

The signing preimage is 0x02 || rlp([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList]) and the broadcast form appends the signature as yParity, r, s. EIP-1559 signatures carry yParity (0/1), not the legacy 27/28 recovery id; encode_signed/2 accepts both and normalizes.

Examples

iex> tx = %X402.Transaction{
...>   chain_id: 1,
...>   nonce: 0,
...>   max_priority_fee_per_gas: 1,
...>   max_fee_per_gas: 2,
...>   gas_limit: 21_000,
...>   to: "0x1111111111111111111111111111111111111111"
...> }
iex> {:ok, preimage} = X402.Transaction.preimage(tx)
iex> Base.encode16(preimage, case: :lower)
"02df0180010282520894111111111111111111111111111111111111111180" <> "80c0"

Summary

Types

Encoding errors.

t()

An EIP-1559 transaction.

Functions

Computes the 32-byte signing digest: keccak256(preimage).

Encodes the broadcast-ready raw transaction from a 65-byte signature.

Returns the unsigned signing preimage: 0x02 || rlp(unsigned fields).

Types

error()

@type error() :: :invalid_address | :invalid_transaction | :invalid_signature_format

Encoding errors.

t()

@type t() :: %X402.Transaction{
  chain_id: non_neg_integer(),
  data: binary(),
  gas_limit: non_neg_integer(),
  max_fee_per_gas: non_neg_integer(),
  max_priority_fee_per_gas: non_neg_integer(),
  nonce: non_neg_integer(),
  to: String.t(),
  value: non_neg_integer()
}

An EIP-1559 transaction.

to is a 0x-prefixed EVM address, data the raw calldata bytes, and all quantities non-negative integers in wei / gas units. The access list is always empty.

Functions

digest(transaction)

(since 0.6.0)
@spec digest(t()) :: {:ok, <<_::256>>} | {:error, error() | :missing_dependency}

Computes the 32-byte signing digest: keccak256(preimage).

Requires the optional ex_keccak dependency and returns {:error, :missing_dependency} when it is unavailable.

encode_signed(transaction, signature)

(since 0.6.0)
@spec encode_signed(t(), binary()) :: {:ok, binary()} | {:error, error()}

Encodes the broadcast-ready raw transaction from a 65-byte signature.

signature is r || s || v where v may be an EIP-1559 yParity (0/1) or a legacy recovery id (27/28, as produced by X402.Signer.sign_eip712/3); both are normalized to yParity. r and s are RLP-encoded as integers, so leading zero bytes are stripped per the Ethereum convention.

Returns the raw bytes to submit via eth_sendRawTransaction (hex-encode with a 0x prefix first).

Examples

iex> tx = %X402.Transaction{
...>   chain_id: 1,
...>   nonce: 0,
...>   max_priority_fee_per_gas: 1,
...>   max_fee_per_gas: 2,
...>   gas_limit: 21_000,
...>   to: "0x1111111111111111111111111111111111111111"
...> }
iex> signature = <<1::unsigned-big-integer-size(256), 2::unsigned-big-integer-size(256), 28>>
iex> {:ok, raw} = X402.Transaction.encode_signed(tx, signature)
iex> Base.encode16(raw, case: :lower)
"02e20180010282520894111111111111111111111111111111111111111180" <> "80c0010102"

iex> tx = %X402.Transaction{
...>   chain_id: 1,
...>   nonce: 0,
...>   max_priority_fee_per_gas: 1,
...>   max_fee_per_gas: 2,
...>   gas_limit: 21_000,
...>   to: "0x1111111111111111111111111111111111111111"
...> }
iex> X402.Transaction.encode_signed(tx, <<0, 1, 2>>)
{:error, :invalid_signature_format}

preimage(transaction)

(since 0.6.0)
@spec preimage(t()) :: {:ok, binary()} | {:error, error()}

Returns the unsigned signing preimage: 0x02 || rlp(unsigned fields).

The digest a signer signs is the keccak-256 hash of this preimage — see digest/1.

Examples

iex> X402.Transaction.preimage(%X402.Transaction{
...>   chain_id: 1,
...>   nonce: 0,
...>   max_priority_fee_per_gas: 1,
...>   max_fee_per_gas: 2,
...>   gas_limit: 21_000,
...>   to: "0xnope"
...> })
{:error, :invalid_address}