Onchain.Tempo.Transaction (onchain_tempo v0.13.1)

Copy Markdown View Source

Tempo transaction decoding, payment matching, and fee payer co-signing.

Summary

Types

t()

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

access()

@type access() :: %{address: binary(), storage_keys: [binary()]}

authorization()

@type authorization() :: %{
  chain_id: non_neg_integer(),
  address: binary(),
  nonce: non_neg_integer(),
  signature: signature()
}

call()

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

call_scope()

@type call_scope() :: %{target: binary(), selector_rules: [selector_rule()]}

key_authorization()

@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
}

key_type()

@type key_type() :: :secp256k1 | :p256 | :webauthn

p256_signature()

@type p256_signature() :: %{
  r: binary(),
  s: binary(),
  pub_key_x: binary(),
  pub_key_y: binary(),
  pre_hash: boolean()
}

primitive_signature()

@type primitive_signature() ::
  {:secp256k1, secp_signature()}
  | {:p256, p256_signature()}
  | {:webauthn, webauthn_signature()}

secp_signature()

@type secp_signature() :: %{
  r: non_neg_integer(),
  s: non_neg_integer(),
  y_parity: 0 | 1
}

selector_rule()

@type selector_rule() :: %{selector: binary(), recipients: [binary()]}

signature()

@type signature() ::
  primitive_signature() | {:keychain, 1 | 2, binary(), primitive_signature()}

t()

@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.

token_limit()

@type token_limit() :: %{
  token: binary(),
  limit: non_neg_integer(),
  period: non_neg_integer()
}

webauthn_signature()

@type webauthn_signature() :: %{
  r: binary(),
  s: binary(),
  pub_key_x: binary(),
  pub_key_y: binary(),
  webauthn_data: binary()
}

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.

hash(tx)

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

Return the transaction hash computed by tempo-primitives.

key_authorization_hash(authorization)

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

Return the signing digest for a typed key authorization.

sender(tx)

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

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(tx)

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

Serialize the named fields through tempo-primitives.

signing_hash(tx)

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

Return the native signing digest as 32 bytes.

simulate_request(tx)

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

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_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}.