Overview

@ethernauta/core

Primitive Valibot schemas for Ethereum base types: addresses, bytes, hashes, unsigned integers, ratios. Every other package composes these. If you find yourself reaching for a regex to validate an address, stop and import from here.

pnpm add @ethernauta/core
import { addressSchema, type Address } from "@ethernauta/core";
import * as v from "valibot";

const address = v.parse(addressSchema, raw_input);
//    ^? Address

Why this package exists

Hard rule 3 in CLAUDE.md: primitive schemas live in @ethernauta/core and nowhere else. Composing them is fine; redeclaring them inside a feature package is not. Centralizing primitives is what keeps the wire-validation surface auditable.

When a new EIP needs a schema for a “32-byte hash,” it imports hash32Schema. It does not write v.pipe(v.string(), v.regex(/^0x[0-9a-f]{64}$/)). If the right primitive is missing, the rule is add it here, not widen the consumer.

The catalog

Addresses

SchemaDescription
addressSchema0x + 40 hex chars. EIP-55 checksum-validated.
addressesSchemaArray of addressSchema.

Types: Address, Addresses.

Bytes

SchemaLengthUse
byteSchema1 byteSingle hex byte (0x + 2 hex chars).
bytesSchemavariableAny 0x-prefixed hex string.
bytes4Schema4 bytesFunction selectors.
bytes8Schema8 bytesEIP-4337 packed gas / fees fields.
bytes32Schema32 bytesHashes, salts, EVM words.
bytes48Schema48 bytesEIP-4844 commitments.
bytes64Schema64 bytesBLS-style signatures.
bytes65Schema65 bytesECDSA signatures (r ‖ s ‖ v).
bytes256Schema256 bytesBloom filters in receipts.
bytesMax32Schema≤ 32 bytesPadded EVM args.

Types: Byte, Bytes, Bytes4, Bytes8, Bytes32, Bytes48, Bytes64, Bytes65, Bytes256, BytesMax32.

Hashes

SchemaDescription
hash32SchemaAlias of bytes32 semantically reserved for keccak hashes.

Type: Hash32.

Unsigned integers

Every standard EVM uint size has a schema. The wire representation is hex-encoded (Ethereum JSON-RPC convention); the parsed JavaScript type is bigint (or number for the small ones — see the inferred type per row).

SchemaRangeTS type
uint8Schema0 – 2⁸ − 1number
uint16Schema0 – 2¹⁶ − 1number
uint24Schema0 – 2²⁴ − 1number
uint32Schema0 – 2³² − 1number
uint40Schema0 – 2⁴⁰ − 1bigint
uint48Schema0 – 2⁴⁸ − 1bigint
uint56Schema0 – 2⁵⁶ − 1bigint
uint64Schema0 – 2⁶⁴ − 1bigint
uint96Schema0 – 2⁹⁶ − 1bigint
uint128Schema0 – 2¹²⁸ − 1bigint
uint160Schema0 – 2¹⁶⁰ − 1bigint
uint192Schema0 – 2¹⁹² − 1bigint
uint224Schema0 – 2²²⁴ − 1bigint
uint256Schema0 – 2²⁵⁶ − 1bigint
uintSchemaany unsignedbigint

Types: Uint8, Uint16, … Uint256, Uint.

Misc

SchemaDescription
ratioSchemaA number between 0 and 1 inclusive.
notFoundSchemaThe sentinel shape for “this RPC returned nothing.”

Types: Ratio, NotFound.

Example: composing

import * as v from "valibot";
import {
  addressSchema,
  bytes32Schema,
  uint256Schema,
} from "@ethernauta/core";

const transferSchema = v.object({
  token: addressSchema,
  from: addressSchema,
  to: addressSchema,
  amount: uint256Schema,
  block_hash: bytes32Schema,
});

type Transfer = v.InferOutput<typeof transferSchema>;

Every field is a primitive from core. There’s no regex anywhere in the consumer. When EIP-55 checksumming improves or bytes32’s wire format changes, this consumer doesn’t change.

Adding a primitive

If you genuinely need a new primitive — say a 17-byte field for a hypothetical new tx type — the right move is to add bytes17Schema to @ethernauta/core, ship it as a normal export, and import it into your consumer. Do not widen with bytesSchema and hand-validate later; parse would have caught the bug at the boundary, and bytesSchema defeats that.

See also