Skip to content

Refactor: Move codec package to Ox - #205

Merged
brunomenezes merged 5 commits into
prerelease/v2-alphafrom
refactor/move-codec-to-ox
Oct 26, 2026
Merged

brunomenezes merged 5 commits into
prerelease/v2-alphafrom
refactor/move-codec-to-ox

Conversation

@brunomenezes

@brunomenezes brunomenezes commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

Summary

Moves @cartesi/codec from viem to ox 1.8.9, the Ethereum primitives library viem itself is built on, and drops viem from the package entirely — runtime, peer and dev.

The package is pure encode/decode with no client, transport or chain machinery at all, yet it carried viem: ^2.0.0 as a peer dependency, so decoding a blob meant npm install @cartesi/codec@alpha viem and pulling in chains, clients, transports and accounts. There is now nothing to install alongside it.

The "breaking" change: data with an unrecognized selector raises a different error class. decodeInput and decodeOutput now throw ox's AbiItem.NotFoundError where they threw viem's AbiFunctionSignatureNotFoundError. Nothing about the signatures changes, so this is invisible at compile time and only shows up where a consumer matches the old class with instanceof.

Every exported type is unchanged. ox's Hex.Hex is `0x${string}`, Bytes.Bytes is Uint8Array, and Address.Address is the abitype Address viem re-exports, so code typing codec values with viem's Hex, ByteArray and Address still compiles.

Only @cartesi/codec moves, all the other packages stay as is.

Commits order characterize(tests) → depend on ox → swap the source → docs, so the suite that proves the migration lands first and stays green across it.

Refactoring

All five files under packages/codec/src/ move to ox. Most of it is mechanical — bytesToBigInt → Bytes.toBigInt, getAddress → Address.checksum, encodePacked → AbiParameters.encodePacked — but four differences are not, and each would be easy to miss in review.

  • AbiFunction.decodeData does not verify the selector when handed a resolved ABI item; it checks Hex.size(data) >= 4 and decodes the rest. viem's decodeFunctionData always resolves the function by the leading four bytes and throws when there is no match. Without a guard, decodeInput would silently decode unknown-selector data as EvmAdvance arguments. Both hex decoders now compare the selector explicitly before decoding, and a test pins that a well-formed blob with the wrong selector throws rather than decodes.
  • ox has no discriminated decode return. decodeOutputHex relied on viem's { functionName, args } union to drive its switch. Replaced by a selector table resolved once at module load, dispatching on Hex.slice(data, 0, 4), so narrowing comes from the literal switch rather than library inference — exact tuple types, no casts. One table now feeds the hex dispatch, the byte array dispatch and the three byte array encoders, where there were two parallel sources before, and a satisfies Record<OutputName, …> makes a fourth Outputs function a compile error.
  • ox has no polymorphic size. Hex.size and Bytes.size are separate where viem's size took either, so assertMinSize — the one genuinely Hex | Uint8Array site — goes through a new byteSize helper in src/bytes.ts.
  • Address checksumming on decode is an option whose default has already moved (false in ox 0.9.6, true in 1.x) where viem's is unconditional. It is passed explicitly on the three calls that decode an address so the value is pinned at the call site; Notice(bytes) carries no address, so it is omitted there.

abitype stays a direct dependency for exactly one type, AbiParameterToPrimitiveType, which ox has no public equivalent for. Its ABI extraction types do move — ExtractAbiFunction → AbiFunction.FromAbi, ExtractAbiFunctionNames → AbiFunction.ExtractNames — and those are literal aliases of the abitype originals. ox and codec both resolve abitype@1.3.0, so the derived types share one instance.

Imports are per-module entrypoints (import * as Hex from "ox/Hex"), not ox's root module. tsdown keeps ox external either way, so no ox code is bundled into codec at all — what differs is what Node loads at runtime. dist/index.cjs references exactly six modules: ox/AbiFunction, ox/AbiItem, ox/AbiParameters, ox/Address, ox/Bytes, ox/Hex. Importing from ox instead would evaluate all 83 modules its root re-exports, pulling in zod, @noble/*, @scure/* and ens-normalize. A bundler with deep scope analysis (Vite, Rollup, webpack 5, esbuild) shakes the unused ones away and the distinction disappears. However, under plain Node, which is who the CommonJS entry point serves, it does not. Measured here: 57 ms and 16 MB of heap to require the built CJS, against 183 ms and 67 MB to require ox's root.

Breaking changes

  • Unknown selector raises AbiItem.NotFoundError (ABI function with data "0xdeadbeef" not found.) instead of viem's AbiFunctionSignatureNotFoundError. Both the hex and the byte array path raise it, as before, and the message still carries the offending selector.
  • A known selector with data too short to decode raises AbiParameters.DataSizeTooSmallError, previously viem's SliceOffsetOutOfBoundsError or PositionOutOfBoundsError depending on how short.
  • The CommonJS entry point requires Node 22.12. ox publishes no CommonJS build, so dist/index.cjs reaches it through Node's require(esm), which is unflagged from that version on. The ESM entry point is unaffected. Verified by packing the tarball and installing it as a real consumer: require() works on 22.12 with no experimental warning, since Node suppresses it for requires originating under node_modules, and fails with ERR_REQUIRE_ESM on 22.11 and below.

The root engines floor also rises, to ^22.18 || ^24.11 || >=26, which is a stricter and unrelated constraint: it is what tsdown@0.23.0 requires. Below 22.18 process.features.typescript is false, tsdown falls back to the optional unrun peer this repo does not install, and pnpm build fails. Root describes what a contributor needs to build and codec's own engines describes what a consumer needs at runtime.

Fixes

  • Coverage reported 0% for every file in three packages. include: ["**/src"] in @cartesi/codec, @cartesi/client and @cartesi/react matched nothing, so the reporters had an empty map and no package had usable coverage. Narrowed to ./src/**. codec additionally excludes src/index.ts, which only re-exports, and src/types.ts, which declares only types — neither carries a statement to cover; client and react keep their existing exclude lists.

Tests

The suite goes from 70 tests in 4 files to 183 in 7, all landing before the source changed.

  • Type-level tests did not exist, while codec's entire public surface is types derived from the ABIs through ParamsToObject — exactly what the migration rewrites. codec.test-d.ts is new, with typecheck enabled in vitest.config.ts as @cartesi/client already does. Its assertions name the structural types — `0x${string}`, Uint8Array, abitype's Address — rather than any library's aliases, which is what makes the pin hold whichever library sits underneath.
  • src/bytes.ts had no suite. The hand-rolled byte readers and writers were covered only through whatever offsets their three callers happened to use. bytes.test.ts reaches the bounds checks, the padding rules and readSize's safe-integer guard, which no test could previously hit.
  • Nothing compared the two implementations. representation.test.ts is a nine-case table asserting the hex and byte array paths agree, for input, the three outputs and the five deposits.
  • Expected payloads are now frozen literals. portal.test.ts rebuilt its expectations at test time with viem's concat/numberToHex/encodeAbiParameters. They are now constants generated from the viem implementation while it was still installed, so they cannot drift with whichever library does the encoding and they read in the diff.
  • Error contracts are centralized. Four bare .toThrow() calls became one helper per file pinning the class and the quoted selector in the message. Also added the sliceFrom boundary at each deposit type's exact minimum size, assertMinSize's full message, and labelled checksum assertions for Voucher.destination on both paths — the existing fixture 0x…01 has no alphabetic digit, so checksumming it was the identity and the assertion could not fail.

PS: The evidence that behaviour is preserved: swapping the source to ox failed exactly five tests, all of them the unknown-selector error class, with the other 178 passing untouched, every frozen payload literal included. The getAddress expectations were frozen to checksummed literals and still pass, which is also what shows ox's Address.checksum agrees with viem's getAddress.
PS²: I opened an issue#524 on Ox GitHub repo regarding add provenance to releases of 1.x as it affected consumers of cartesi/codec. However, only when these consumers have the trustPolicy: no-downgrade enabled (e.g. which rollups-explorer has)

Pins the derived types, the hand-rolled byte readers and writers, and every expected payload as a frozen literal, so a change of encoding library has to preserve them.
@changeset-bot

changeset-bot Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 191c166

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cartesi/codec Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cartesi-playground Ready Ready Preview Oct 23, 2026 11:27am UTC

Request Review

Comment thread pnpm-workspace.yaml Resolved
@brunomenezes brunomenezes moved this to 👁 In Review in Rollups Tooling Oct 23, 2026
viem stays a devDependency until the source stops importing it. Ox ships no CommonJS build, so the require entry point needs Node 22.12 or newer.
Unknown selectors now raise ox's AbiItem.NotFoundError. Ox has no polymorphic size and does not resolve a function by selector when handed a resolved ABI item, so portal gains a byteSize helper and both hex decoders guard the selector explicitly.
Also notes that the CommonJS entry point needs Node 22.12, since ox ships no CommonJS build.
@github-project-automation github-project-automation Bot moved this from 👁 In Review to 👍 Waiting Merge in Rollups Tooling Oct 23, 2026
@brunomenezes
brunomenezes merged commit a2f9239 into prerelease/v2-alpha Oct 26, 2026
6 checks passed
@brunomenezes
brunomenezes deleted the refactor/move-codec-to-ox branch October 26, 2026 09:59
@github-project-automation github-project-automation Bot moved this from 👍 Waiting Merge to 📦 Done in Rollups Tooling Oct 26, 2026

This branch was successfully deployed

1 active deployment
Preview — 191c1664 Deployed Oct 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: 📦 Done

Development

Successfully merging this pull request may close these issues.

2 participants