TypeScript for the current EIP-8141 frame transaction envelope implemented by
the Ethrex frames devnet (chain ID 81410).
Decode, build, hash, sign, price, dry-run and broadcast.
bun add @jaw.id/frametx-kit viemviem is a peer dependency, not a bundled one. frameActions extends a viem client, so
your client and this library have to be the same viem.
import { createPublicClient, http } from 'viem'
import { frameActions } from '@jaw.id/frametx-kit/viem'
const client = createPublicClient({
transport: http('https://rpc1.frames.ethrex.xyz'),
}).extend(frameActions)
const tx = await client.getFrameTransaction({ hash })
const receipt = await client.getFrameTransactionReceipt({ hash })Building a transaction? Call assertValidFrameTx(tx) before encodeFrameTx(tx). The
encoder itself does not validate, because sig_hash is defined over a re-encoding and a
validating encoder would reject the live chain data this library is meant to survey.
import { assertValidFrameTx, encodeFrameTx, signFrameTx } from '@jaw.id/frametx-kit'
const signed = await signFrameTx(tx, privateKey)
assertValidFrameTx(signed)
const raw = encodeFrameTx(signed)signFrameTx also takes a FrameSigner instead of a key — anything with an address and a
raw-digest sign, which covers viem's privateKeyToAccount and mnemonicToAccount, a
toAccount source, and your own wrapper around a hardware wallet, an HSM or a remote
signer. It signs every SECP256K1 entry whose msg is empty, and refuses if the entry's
resolved signer is not that account's address.
import { privateKeyToAccount } from 'viem/accounts'
const signed = await signFrameTx(tx, privateKeyToAccount(privateKey))
// a remote signer needs nothing else:
const signed = await signFrameTx(tx, {
address: '0x…',
sign: ({ hash }) => myKms.signDigest(hash), // r||s||v, v as 27/28 or a bare 0/1
})A raw-digest sign is required. signMessage will not do: it EIP-191-prefixes its
argument, so the signature recovers to nothing. A JsonRpcAccount (a browser wallet)
cannot sign a raw digest at all — both are refused with an error rather than producing a
transaction the chain silently rejects.
P256 and ARBITRARY entries are left untouched; build those signatures yourself and let
assertValidFrameTx check them. signFrameTx also handles one signer at a time, so a
transaction with entries for two different signers needs the bytes assembled by hand.
Sending walks the strict path for you — assertValidFrameTx, encodeFrameTx,
eth_sendRawTransaction — and checks that the hash the node returns is keccak256 of the
bytes it was given. The receipt wait is frame-aware: a skipped frame comes back as
'skipped', not as a revert.
const hash = await client.sendFrameTransaction({ transaction: signed })
const receipt = await client.waitForFrameTransactionReceipt({ hash })Passing every check here does not guarantee admission. On this chain the VERIFY prefix must
call APPROVE, which a plain EOA sender cannot do, so a broadcastable transaction needs a
sender contract and a funded key — both are yours, not this library's. Dry-run with
client.simulateFrameTransaction({ raw }) before spending.
Frame receipt status is three-valued — 'failure', 'success', 'skipped'. A
skipped frame never executed and its gas was refunded. It is not a revert.
The chain's public RPC serves no raw transaction bytes. getFrameTransaction reads
the node's decoded JSON, re-encodes it, and refuses to return anything whose re-encoding
does not reproduce the transaction hash. What you get back is a transaction this library
can put back on the wire byte-for-byte.
frameTxGas prices the current EIP-8141 transaction shape. The historical
'chain', 'pins', and 'head' rule-set names remain accepted as compatibility
aliases and currently produce identical results.
import { compareRuleSets, decodeFrameTx } from '@jaw.id/frametx-kit'
console.log(compareRuleSets(decodeFrameTx(raw), 'chain', 'pins'))Every gas constant is written as its published figure rather than derived. ethrex's own suite missed its intrinsic dropping from 15000 to 12000 across 1372 tests by deriving the expected value from the constant under test.
bun run test # hermetic, no network
bun run typecheckThe default suite is hermetic. Its golden RLP and signature hash are pinned to the
current Ethrex FrameTransaction encoder.
Contributions welcome. Read CONTRIBUTING.md first — this wire format
has traps, and several things in src/ that look like code smells are load-bearing.
The most valuable report is a disagreement with the reference client. If this library and ethrex produce different bytes, a different hash, a different price or a different receipt, open a divergence report with the transaction hash or the byte vector. That is what a second implementation is for.
Anything that could let a transaction be authorised for something other than what it says
goes to SECURITY.md and a private advisory instead of a public issue.
Looking for somewhere to start? The issues labelled good first issue are the coverage gaps that need a captured transaction rather than a design decision.
CONTRIBUTING.md— architecture, invariants, wire-format traps, how the verification oracles workdocs/DESIGN.md— the binding design specdocs/OPEN-ITEMS.md— known gaps and unfinished workSECURITY.md— what counts as a vulnerability here, and how to report itCODE_OF_CONDUCT.md— expected conduct in issues and pull requests
MIT