Onchain.Tempo.Transaction (onchain_tempo v0.2.0)

Copy Markdown View Source

Tempo Transaction (EIP-2718 type 0x76) — RLP deserialization, payment call matching, and fee payer co-signing (0x78 domain).

A Tempo Transaction is an RLP-encoded envelope prefixed with 0x76:

0x76 || rlp([chain_id, max_priority_fee_per_gas, max_fee_per_gas, gas_limit,
              calls, access_list, nonce_key, nonce, valid_before, valid_after,
              fee_token, fee_payer_signature, aa_authorization_list,
              key_authorization?, sender_signature])

Each call is rlp([to, value, input]).

This module extracts the fields needed for payment verification (chain_id, calls) and preserves the full serialized hex as raw for broadcast passthrough. When fee payer mode is enabled, it co-signs the transaction with a server-side key using the 0x78 domain separator.

Dependencies

Uses ExRLP (available transitively via signetonchain) for RLP decoding. Signing uses Signet.Signer.Curvy and Signet.Recover directly because Tempo 0x76 is a non-standard transaction type.

Summary

Types

A single call within the transaction's batch.

t()

A parsed Tempo Transaction with verification-relevant fields.

Functions

Co-sign a client's transaction as fee payer and return the updated hex.

Deserialize a hex-encoded Tempo Transaction (0x76 prefix).

Check if the transaction's fee_token field is empty (RLP null).

Find a matching payment call (transfer or transferWithMemo) in the transaction.

Check if the transaction has a fee payer signature placeholder (0x00).

Validate that a transaction's calls match an allowed fee-payer pattern.

Types

call()

@type call() :: %{to: binary(), value: non_neg_integer(), input: binary()}

A single call within the transaction's batch.

t()

@type t() :: %Onchain.Tempo.Transaction{
  calls: [call()],
  chain_id: non_neg_integer(),
  fields: [term()],
  raw: String.t()
}

A parsed Tempo Transaction with verification-relevant fields.

raw is the full serialized transaction as a hex string ("0x76...") with 0x prefix, suitable for direct JSON-RPC broadcast.

Functions

cosign_fee_payer(tx, fee_payer_key, fee_token)

@spec cosign_fee_payer(t(), binary(), binary()) :: {:ok, t()} | {:error, String.t()}

Co-sign a client's transaction as fee payer and return the updated hex.

Takes the client-signed 0x76 transaction, adds the server's fee payer signature (domain 0x78), injects the fee token, and returns a new 0x76 hex string ready for broadcast.

Parameters

  • tx — deserialized transaction with fee payer placeholder
  • fee_payer_key — 32-byte binary private key for fee sponsorship
  • fee_token — 20-byte binary TIP-20 token address for fee payment

Returns

  • {:ok, updated_tx} — transaction with new raw hex for broadcast
  • {:error, reason} — on signing or recovery failure

deserialize(hex)

@spec deserialize(String.t()) :: {:ok, t()} | {:error, String.t()}

Deserialize a hex-encoded Tempo Transaction (0x76 prefix).

Returns {:ok, %Transaction{}} with chain_id, parsed calls, and the original hex string as raw (for broadcast). Returns {:error, reason} on invalid input.

Examples

iex> Onchain.Tempo.Transaction.deserialize("0x76" <> valid_rlp_hex)
{:ok, %Onchain.Tempo.Transaction{chain_id: 42431, calls: [...], raw: "0x76..."}}

iex> Onchain.Tempo.Transaction.deserialize("0x02" <> rlp_hex)
{:error, "Not a Tempo transaction: expected 0x76 type prefix"}

fee_token_empty?(transaction)

@spec fee_token_empty?(t()) :: boolean()

Check if the transaction's fee_token field is empty (RLP null).

Clients leave fee_token empty when feePayer: true, allowing the server to choose the fee payment token.

find_payment_call(transaction, currency, opts)

@spec find_payment_call(t(), String.t(), keyword()) ::
  {:ok, map()} | {:error, String.t()}

Find a matching payment call (transfer or transferWithMemo) in the transaction.

Searches tx.calls for one targeting currency with the correct selector, then ABI-decodes and verifies recipient, amount, and optional memo.

Options

  • :amount — (required) expected amount as string
  • :recipient — (required) expected recipient as hex address
  • :memo — (optional) bytes32 hex memo; when set, MUST match transferWithMemo

has_fee_payer_placeholder?(transaction)

@spec has_fee_payer_placeholder?(t()) :: boolean()

Check if the transaction has a fee payer signature placeholder (0x00).

Per spec, clients set fee_payer_signature to 0x00 when requesting server-side fee sponsorship.

validate_call_scope(transaction)

@spec validate_call_scope(t()) :: :ok | {:error, String.t()}

Validate that a transaction's calls match an allowed fee-payer pattern.

Fee-payer sponsored transactions are restricted to specific call sequences to prevent clients from bundling rogue calls that the server would pay gas for.

Allowed patterns (matching mppx callScopes):

  • [transfer]
  • [transferWithMemo]
  • [approve, swapExactAmountOut, transfer]
  • [approve, swapExactAmountOut, transferWithMemo]

When approve is present, the spender must be the stablecoin DEX. When swapExactAmountOut is present, the call target must be the stablecoin DEX.

Returns :ok or {:error, reason}.