Frame Transactions (EIP-8141)
Overview
Use TxEnvelopeEip8141 to build a transaction containing
multiple Frame calls. Each frame has its own execution mode, target,
calldata, value, and gas budgets. The transaction carries a shared
FrameSignature list rather than an outer ECDSA signature.
Build, Sign & Send
Create a verification frame that approves execution and payment, followed by a sender
frame that transfers value. Omitting a frame's to selects the transaction sender.
The example assumes a chain ID, sender nonce, and configured RPC transport. Choose fees and per-frame budgets for your network; the values below illustrate the flow on a development network.
import {
Address,
Hex,
RpcTransport,
Secp256k1,
TxEnvelopeEip8141,
Value,
} from 'ox'
declare const chainId: number
declare const nonce: bigint
declare const privateKey: Hex.Hex
declare const recipient: Address.Address
declare const rpc: RpcTransport.Http
const sender = Address.fromPublicKey(Secp256k1.getPublicKey({ privateKey }))
const envelope = TxEnvelopeEip8141.from({
chainId,
frames: [
{
executionGas: 50_000n,
flags: 'approveExecutionAndPayment',
mode: 'verify',
},
{
executionGas: 50_000n,
mode: 'sender',
to: recipient,
value: Value.fromEther('0.001'),
},
],
maxFeePerGas: Value.fromGwei('10'),
maxPriorityFeePerGas: Value.fromGwei('1'),
nonce,
sender,
signatures: [{ scheme: 'secp256k1' }],
})
const signature = Secp256k1.sign({
payload: TxEnvelopeEip8141.getSignPayload(envelope),
privateKey,
})
const signed = TxEnvelopeEip8141.from({
...envelope,
signatures: [{ scheme: 'secp256k1', signature }],
})
const hash = await rpc.request({
method: 'eth_sendRawTransaction',
params: [TxEnvelopeEip8141.serialize(signed)],
})Choose Frame Modes and Budgets
Frame.from accepts named modes or their numeric equivalents:
| Mode | Value | Purpose |
|---|---|---|
default | 0 | Execute a call as the protocol entry point. |
verify | 1 | Validate the transaction. |
sender | 2 | Execute a call as the transaction sender. |
Use executionGas for execution and stateGas for state creation. Both default to
zero; provide budgets appropriate for each call. A transfer to an existing account
avoids new-account state costs. Contract calls that create storage or accounts can need
stateGas as well.
Additional sender frames can carry contract calldata in data. Calls do not become
an atomic batch merely by sharing a transaction. Use flags: 'atomicBatch' on each
frame that joins the following frame to the batch, leaving the final frame unflagged.
Verification frames cannot be part of an atomic batch.
Sign the Complete Envelope
Create the signature entries before calling getSignPayload, including their schemes
and any explicit signers. An empty payload, the default, selects the canonical
transaction signing hash. Attaching the resulting signature bytes preserves that hash.
Changing frames, fees, nonce, or signature metadata requires signing again.
Signature entries do not correspond to frames by array position. They form a shared
list available to account verification logic. FrameSignature.from supports
'arbitrary', 'secp256k1', and 'p256'; omitting scheme selects 'arbitrary'.
Use an explicit 32-byte payload only when the account expects a separate digest.
Convert RPC Data
TxEnvelopeEip8141.toRpc converts sender to
from, frame to to target, and signature payload to msg. Frame gas fields
remain executionGas and stateGas. RPC frame modes, flags, signature schemes,
and gas and fee quantities are hex strings. Protocol signature placeholders omit
signature.
import { TxEnvelopeEip8141 } from 'ox'
declare const envelope: TxEnvelopeEip8141.TxEnvelopeEip8141
const request = TxEnvelopeEip8141.toRpc(envelope)
const restored = TxEnvelopeEip8141.fromRpc(request)
const serialized = TxEnvelopeEip8141.serialize(restored)Inspect Frame Receipts
Convert the transaction receipt with
TransactionReceipt.fromRpc to read the payer
and individual frame results. Each frame receipt exposes executionGasUsed,
stateGasUsed, total gasUsed, logs, and a status of 'success', 'reverted',
or 'skipped'.
import { Hex, RpcTransport, TransactionReceipt } from 'ox'
declare const hash: Hex.Hex
declare const rpc: RpcTransport.Http
const receipt = TransactionReceipt.fromRpc(
await rpc.request({
method: 'eth_getTransactionReceipt',
params: [hash],
}),
)
const payer = receipt?.payer
const frames = receipt?.frameReceiptsUse FrameReceipt.fromRpc when converting an
individual frame receipt separately.

