Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/ISSUE_TEMPLATE/03-feature.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ body:
- type: markdown
attributes:
value: |
Check `docs/OPEN-ITEMS.md` first. Known gaps are already recorded there, and a comment on the matching item is more useful than a new issue.
Check the open issues first; a comment on a matching one is more useful than a new issue.
- type: textarea
id: need
attributes:
Expand All @@ -28,7 +28,8 @@ body:
- sighash
- signatures
- gas
- divergence
- nonce
- accounts
- rlp
- rpc
- viem
Expand Down
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Which oracle covers this change, and why it could actually fail:
- [ ] A live receipt (recovered signer, `maxCost`, or the `gasUsed` decomposition)
- [ ] N/A, this change has no wire-format or gas behaviour

<!-- A test that asserts the code's own output against itself does not count. See CONTRIBUTING.md, "Verification". -->
<!-- A test that asserts the code's own output against itself does not count. See CONTRIBUTING.md, "Tests". -->

## Load-bearing rules

Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ jobs:
sighash
signatures
gas
divergence
nonce
accounts
rlp
rpc
viem
Expand Down
51 changes: 27 additions & 24 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ exist only to redirect assistants here.
`frametx-kit` reads, builds, hashes, signs, prices, simulates and broadcasts EIP-8141
frame transactions as the hegota-testnet chain (chain ID `8141`) accepts them. That
envelope is a composition of three draft EIPs that none of them specifies on its own:
EIP-8141 frames, EIP-8250 keyed nonces, EIP-8272 recent-root references.
EIP-8141 frames, EIP-8250 keyed nonces, EIP-8272 recent-root references (which now travel
in a VERIFY frame rather than an envelope field).

It is a deliberate second implementation. The migration is pinned against
[ethrex commit `d587cf9`](https://github.com/lambdaclass/ethrex/tree/d587cf9ff0996315381c4b2784a4d7d499decc0f),
It is a deliberate second implementation. It is pinned against
[ethrex commit `bdfc5d8`](https://github.com/lambdaclass/ethrex/tree/bdfc5d8f2e7f653e620a9901db915f89a87a3d0d),
the `hegota-testnet` build that `rpc1.privacy.ethrex.xyz` serves,
and this library exists partly to disagree with it usefully. Every `.rs`, `.py` and
`docs/*.md` path cited in this repository's docs and comments — `transaction.rs`,
`frame_tx_wire_tests.rs`, `frametx.py` and the rest — is a path in that repository, not in
Expand Down Expand Up @@ -46,14 +48,13 @@ interchangeable.

```bash
bun run test # hermetic, no network
bun run test:live # checks rpc1.frames.ethrex.xyz identity
bun run test:live # checks rpc1.privacy.ethrex.xyz identity
bun run typecheck
bun run build # tsup -> dist/, dual ESM + CJS with both declaration flavours
```

CI runs `typecheck`, `test`, `build`, and then `@arethetypeswrong/cli --pack .` against
the built package. All four must pass. The public frames RPC currently has no
frame-aware simulation method, so there is no live simulation gate.
the built package. All four must pass.

That last check is there because a broken `exports` map or a mismatched `.d.cts` breaks
every consumer while leaving the test suite entirely green — the tests import from `src/`,
Expand Down Expand Up @@ -93,6 +94,11 @@ path is `assertValidFrameTx(tx)` then `encodeFrameTx(tx)`.

## Traps specific to this wire format

- **There is no scalar nonce.** The envelope's second and third fields are `nonceKeys`
(an RLP list, even for the common `[0]`) and `nonceSeq`. Key `0` is only valid alone.
`rlp(nonce_keys) || rlp(nonce_seq)` is billed as calldata, so dropping it misprices
every transaction by at least 48 gas — `test/chain.oracle.test.ts` catches that against
the node's `maxCost`.
- **Signature layout is `v || r || s`**, `v` at byte 0 as a bare recovery id (0 or 1),
never 27/28. That is the reverse of viem's layout in both directions. A 27/28-encoded
signature makes the transaction invalid at consensus, not merely unrelayable.
Expand Down Expand Up @@ -123,25 +129,25 @@ Historical state is pruned. `ethrex_simulateFrameTransaction` works only against
`rlp` / `errors` / `types` → `envelope` → `sighash` → `signatures`. `gas` depends only on
`rlp` and `types`. `rpc` depends only on `errors` and `types`. `viem` sits on top — it
composes `envelope`, `signatures`, `rpc` and `gas` and holds no wire logic of its own.
`divergence` depends on `gas`. Nothing imports upward.
Nothing imports upward.

## Verification
## Tests

Three oracles, and none of them is self-referential:
Wire-format and gas tests are pinned to something other than the code's own output:

1. The golden byte vector and sig-hash, transcribed from ethrex's Rust tests.
2. Re-encoding the node's decoded JSON must reproduce each transaction's hash.
3. Against live receipts: recovered signer equals the *resolved* signer (often not
`sender` on this chain), our `'chain'` `maxCost` equals the node's, and
`gasUsed = intrinsic + Σ frame execution + Σ frame state`.
- `test/fixtures/golden.ts` — the golden vector and sig-hash from ethrex's
`frame_tx_wire_tests.rs`.
- `test/fixtures/chain/` — real transactions captured from the chain, with their receipts
and the node's simulate answer. `test/chain.oracle.test.ts` re-encodes each one and checks
the hash, the recovered signer, `maxCost` against the node's, and receipt gas. To add one,
run `bunx tsx scripts/capture-fixtures.ts 0x<hash>`. Pick transactions without storage
refunds: receipts don't itemize them, so their gas can't be reconstructed.
- `test/gas.test.ts` — expected figures derived by hand, with the derivation in comments.

A round-trip test cannot catch a mistake mirrored in the encoder and the decoder, such as
the `v || r || s` byte order, so pin any new layout to bytes or to captured data as well.
Run the commands and read the output before concluding that something works.

The `v || r || s` byte order deserves a note, because a round-trip test cannot pin it: a
mirrored error in the encoder and the decoder survives one. It is pinned instead by
hand-constructed byte-pattern assertions and by oracle 3 recovering signers from real
ethrex-produced signatures. Hold any new layout you add to the same standard.

## Commits, PR titles and releases

This repository releases automatically once it has been released once. Merging to `main`
Expand All @@ -165,9 +171,9 @@ That means **your commit messages set the version**. Use
| `docs:`, `chore:`, `test:`, `ci:`, `refactor:`, `style:`, `perf:`, `build:`, `revert:` | no release |

Pull request titles are validated by CI against that list, with an optional scope from
`envelope`, `sighash`, `signatures`, `gas`, `divergence`, `rlp`, `rpc`, `viem`,
`envelope`, `sighash`, `signatures`, `gas`, `nonce`, `accounts`, `rlp`, `rpc`, `viem`,
`fixtures`, `docs`, `deps`, `ci`, `repo` — for example
`fix(envelope): encode the scalar frame nonce`. Subjects start lowercase.
`fix(envelope): reject a zero nonce key beside other keys`. Subjects start lowercase.

Do not hand-edit `version` in `package.json` or touch `CHANGELOG.md`; the release commit
owns both. A wire-format or gas change is almost always at least a `fix:`, because
Expand All @@ -181,14 +187,11 @@ someone downstream is encoding bytes with this.
published figure, a golden vector, or captured chain data, not to the code's own output.
- If you touched anything in **Rules that are not style preferences**, say why in the PR
description. Those changes are not refused, but they are argued.
- Files under `test/fixtures/chain` are archived captures from the superseded envelope.
Do not use them as current wire-format oracles.

## Where the rest of the docs are

- `docs/DESIGN.md` — the binding design spec. It is the authority on intended behaviour;
where the code and the spec disagree, that is a bug in one of them, so say which.
- `docs/OPEN-ITEMS.md` — known gaps and unfinished work.
- `README.md` — user-facing API.
- `SECURITY.md` — what counts as a vulnerability here, and why a report goes private.
- `CODE_OF_CONDUCT.md` — expected conduct in issues and pull requests.
Expand Down
39 changes: 26 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/JustaLab-co/frametx-kit/badge)](https://scorecard.dev/viewer/?uri=github.com/JustaLab-co/frametx-kit)

TypeScript for the current **EIP-8141 frame transaction** envelope implemented by
the Ethrex frames devnet (chain ID `81410`).
TypeScript for the **EIP-8141 frame transaction** envelope, with EIP-8250 keyed nonces,
as the Ethrex hegota-testnet (chain ID `8141`) accepts it.

Decode, build, hash, sign, price, dry-run and broadcast.

Expand All @@ -23,10 +23,12 @@ your client and this library have to be the same viem.

```ts
import { createPublicClient, http } from 'viem'
import { hegotaTestnet } from '@jaw.id/frametx-kit'
import { frameActions } from '@jaw.id/frametx-kit/viem'

const client = createPublicClient({
transport: http('https://rpc1.frames.ethrex.xyz'),
chain: hegotaTestnet,
transport: http(),
}).extend(frameActions)

const tx = await client.getFrameTransaction({ hash })
Expand Down Expand Up @@ -82,11 +84,23 @@ 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
Passing every check here does not guarantee admission. The VERIFY prefix must `APPROVE`
payment, and the payer must hold the transaction's `maxCost`, or the frame reverts. A
code-less EOA gets that from the protocol's default code, which `toEoaFrameAccount` targets;
anything else needs its own validating contract. Dry-run with
`client.simulateFrameTransaction({ raw })` before spending.

## Nonces are keyed

A transaction carries `nonceKeys` and one `nonceSeq` (EIP-8250), not a scalar `nonce`.
`[0n]` is the ordinary account nonce and is what `prepareFrameTransaction` uses by
default. Any other key lives in the `NONCE_MANAGER` predeploy; `getFrameNonceSeq` reads
either kind, and `prepareFrameTransaction(account, calls, limits, { nonceKeys: [1n, 2n] })`
selects several, provided they currently sit at the same sequence.

The first use of a non-zero key costs `KEYED_NONCE_FIRST_USE_STATE_GAS` (97,920) of state
gas, charged against the payment-approving frame's `limits.state`. Budget for it.

## Two things that will bite you

**Frame receipt status is three-valued** — `'failure'`, `'success'`, `'skipped'`. A
Expand All @@ -99,13 +113,13 @@ can put back on the wire byte-for-byte.

## Gas

`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.
`frameTxGas` prices a transaction from its declared limits, including EIP-8250's nonce
calldata, and `frameTxMaxCost` gives the most it can cost the payer.

```ts
import { compareRuleSets, decodeFrameTx } from '@jaw.id/frametx-kit'
console.log(compareRuleSets(decodeFrameTx(raw), 'chain', 'pins'))
import { frameTxGas, frameTxMaxCost } from '@jaw.id/frametx-kit'
const { intrinsicGas, maxGas } = frameTxGas(tx)
const maxCost = frameTxMaxCost(tx, blobBaseFee)
```

Every gas constant is written as its published figure rather than derived. ethrex's own
Expand Down Expand Up @@ -142,9 +156,8 @@ are the coverage gaps that need a captured transaction rather than a design deci
## Docs

- [`CONTRIBUTING.md`](CONTRIBUTING.md) — architecture, invariants, wire-format traps, how
the verification oracles work
the tests work
- [`docs/DESIGN.md`](docs/DESIGN.md) — the binding design spec
- [`docs/OPEN-ITEMS.md`](docs/OPEN-ITEMS.md) — known gaps and unfinished work
- [`SECURITY.md`](SECURITY.md) — what counts as a vulnerability here, and how to report it
- [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) — expected conduct in issues and pull requests

Expand Down
Loading
Loading