EIPs

EIP-712 — Typed structured data hashing and signing

eips.ethereum.org/EIPS/eip-712

EIP-712 lets a dapp ask the user to sign structured data — domain-bound, schema-defined, human-readable in the wallet popup. Where personal_sign (EIP-191) signs an opaque byte string, EIP-712 signs a typed object whose hash is computed from declared field types.

import { hash_typed_data, type TypedData } from "@ethernauta/eip/712";
import { AddressSchema, Uint256Schema } from "@ethernauta/core";
import { parse } from "valibot";

const owner = parse(AddressSchema, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
const spender = parse(AddressSchema, "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC");
const value = parse(Uint256Schema, "0x3e8");
const nonce = parse(Uint256Schema, "0x0");
const deadline = parse(Uint256Schema, "0xffffffff");

const typed: TypedData = {
  types: {
    Permit: [
      { name: "owner", type: "address" },
      { name: "spender", type: "address" },
      { name: "value", type: "uint256" },
      { name: "nonce", type: "uint256" },
      { name: "deadline", type: "uint256" },
    ],
  },
  primaryType: "Permit",
  domain: {
    name: "USD Coin",
    version: "2",
    chainId: 1,
    verifyingContract: parse(AddressSchema, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"),
  },
  message: { owner, spender, value, nonce, deadline },
};

const digest = hash_typed_data(typed);

Surface

ExportTypePurpose
TypedDataSchemaValibot schemaValidate a typed-data payload.
TypedData, TypedDataDomain, TypedDataFieldtypesInferred shapes.
hash_typed_data(td: TypedData) => Hash32EIP-712 digest.
eth_signTypedData_v4Signable<Bytes>Wallet-side signing method binding.

Signing via the wallet

import { eth_signTypedData_v4, type TypedData } from "@ethernauta/eip/712";
import type { Provider } from "@ethernauta/eip/1193";
import { create_provider, encode_chain_id } from "@ethernauta/transport";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { AddressSchema } from "@ethernauta/core";
import { parse } from "valibot";

const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
declare const provider: Provider; // see /eips/6963 for discovery
const { signer } = create_provider(provider);

const account = parse(AddressSchema, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
const typed_data: TypedData = {
  domain: { name: "Example", version: "1", chainId: 1 },
  types: { Mail: [{ name: "from", type: "address" }] },
  primaryType: "Mail",
  message: { from: account },
};

const signature = await eth_signTypedData_v4([account, typed_data])(
  signer({ chain_id: CHAIN_ID }),
);

The signer wraps a 1193 provider; the wallet opens its sign-typed-data view, displays the field-by-field breakdown, and returns the 65-byte signature.

Verification

import { verify_typed_data } from "@ethernauta/crypto";
import { create_reader, encode_chain_id, http } from "@ethernauta/transport";
import type { TypedData } from "@ethernauta/eip/712";
import { eip155_1 } from "@ethernauta/chain/eip155-1";
import { AddressSchema, BytesSchema } from "@ethernauta/core";
import { parse } from "valibot";

const CHAIN_ID = encode_chain_id({ namespace: "eip155", reference: eip155_1.chainId });
const reader = create_reader([
  { chainId: CHAIN_ID, transports: [http("https://ethereum-rpc.publicnode.com")] },
]);

const address = parse(AddressSchema, "0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
const signature = parse(BytesSchema, "0x");
const typedData: TypedData = {
  domain: { name: "Example", version: "1", chainId: 1 },
  types: { Mail: [{ name: "from", type: "address" }] },
  primaryType: "Mail",
  message: { from: address },
};

const ok = await verify_typed_data({
  address,
  typedData,
  signature,
})(reader({ chain_id: CHAIN_ID }));

Same EOA → 1271 fallback as verify_message. Add _universal for counterfactual.

Common consumers

  • ERC-2612 permit — gasless ERC-20 approvals.
  • EIP-4361 SIWE — sign-in with Ethereum.
  • ERC-7683 — cross-chain order signing.
  • EIP-7702 authorizations — set-code delegations.

Each of those packages composes the hash_typed_data primitive rather than reimplementing the digest.

See also