Tempo transaction decoding, payment matching, and fee payer co-signing.
Summary
Types
Named transaction fields. Addresses and data are binaries; raw is broadcast-ready hex. Builder-assembled transactions carry nil signature and raw until signed.
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).
Return the transaction hash computed by tempo-primitives.
Return the signing digest for a typed key authorization.
Recover the sender's 20-byte address from a parsed transaction.
Serialize the named fields through tempo-primitives.
Return the native signing digest as 32 bytes.
Build an eth_simulateV1 call request (a TempoTransactionRequest) from a
co-signed transaction.
Validate that a transaction's calls match an allowed fee-payer pattern.
Types
@type authorization() :: %{ chain_id: non_neg_integer(), address: binary(), nonce: non_neg_integer(), signature: signature() }
@type call() :: %{to: binary() | nil, value: non_neg_integer(), input: binary()}
@type call_scope() :: %{target: binary(), selector_rules: [selector_rule()]}
@type key_authorization() :: %{ chain_id: non_neg_integer(), key_type: key_type(), key_id: binary(), expiry: pos_integer() | nil, limits: [token_limit()] | nil, allowed_calls: [call_scope()] | nil, witness: binary() | nil, is_admin: boolean(), account: binary() | nil, signature: primitive_signature() | nil }
@type key_type() :: :secp256k1 | :p256 | :webauthn
@type primitive_signature() :: {:secp256k1, secp_signature()} | {:p256, p256_signature()} | {:webauthn, webauthn_signature()}
@type secp_signature() :: %{ r: non_neg_integer(), s: non_neg_integer(), y_parity: 0 | 1 }
@type signature() :: primitive_signature() | {:keychain, 1 | 2, binary(), primitive_signature()}
@type t() :: %Onchain.Tempo.Transaction{ access_list: [access()], calls: [call()], chain_id: non_neg_integer(), fee_payer_signature: secp_signature() | :placeholder | nil, fee_token: binary() | nil, gas_limit: non_neg_integer(), key_authorization: key_authorization() | nil, max_fee_per_gas: non_neg_integer(), max_priority_fee_per_gas: non_neg_integer(), nonce: non_neg_integer(), nonce_key: non_neg_integer(), raw: String.t() | nil, signature: signature() | nil, tempo_authorization_list: [authorization()], valid_after: pos_integer() | nil, valid_before: pos_integer() | nil }
Named transaction fields. Addresses and data are binaries; raw is broadcast-ready hex. Builder-assembled transactions carry nil signature and raw until signed.
@type token_limit() :: %{ token: binary(), limit: non_neg_integer(), period: non_neg_integer() }
Functions
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 placeholderfee_payer_key— 32-byte binary private key for fee sponsorshipfee_token— 20-byte binary TIP-20 token address for fee payment
Returns
{:ok, updated_tx}— transaction with newrawhex for broadcast{:error, reason}— on signing or recovery failure
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"}
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 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
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.
Return the transaction hash computed by tempo-primitives.
@spec key_authorization_hash(key_authorization()) :: {:ok, binary()} | {:error, String.t()}
Return the signing digest for a typed key authorization.
Recover the sender's 20-byte address from a parsed transaction.
Handles both self-signed and fee-payer co-signed transactions. For a
co-signed transaction the sender signed over placeholder fee_token (<<>>)
and fee_payer_signature (<<0x00>>) — the fee payer fills those in
afterward — so they are reset before the signing payload is reconstructed.
Keychain recovery verifies the inner signature and returns the root account. It does not establish that the access key is authorized on-chain; callers must check the AccountKeychain precompile for that authorization.
High-s Secp256k1 encodings are accepted: s and the recovery bit are flipped together
to BIP-62 low-s form before recovery, so a complement-s envelope returns the
same address. The original raw is not rewritten.
Returns {:ok, address_binary} or {:error, reason}.
Serialize the named fields through tempo-primitives.
Return the native signing digest as 32 bytes.
Build an eth_simulateV1 call request (a TempoTransactionRequest) from a
co-signed transaction.
Mirrors mpp-rs's build_simulate_payload: the recovered sender becomes from,
and the LAST sub-call is folded into to/value/input (so the node does not
read an empty to as a contract CREATE) while the remaining N-1 calls stay in
calls. All numeric fields are hex-quantity strings; nonceKey and
validBefore are omitted when zero.
The single-call wire format is integration-verified against Moderato; the
multi-call path mirrors the same per-call {to, value, input} shape.
Returns {:ok, request_map} or {:error, reason}.
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}.