Overview

@ethernauta/crypto

Universal signature verification across EIP-191, EIP-712, EIP-1271, and EIP-6492 — plus the underlying ECDSA / hashing / key-derivation primitives.

pnpm add @ethernauta/crypto

This is the package that lets you verify a signature without caring whether it was produced by an EOA, a deployed smart-account, or a counterfactual (not-yet-deployed) smart-account. The “universal” verifiers walk the full hierarchy.

Verifying messages (EIP-191)

import { verify_message } from "@ethernauta/crypto";

const ok = await verify_message({
  address: signer,
  message: "Hello, world",
  signature,
})(reader({ chain_id: eip155_1.chain_id }));

verify_message tries EOA recovery, then EIP-1271 if the address has code. verify_message_deployed is EIP-1271-only; verify_message_universal adds EIP-6492 (works for not-yet-deployed accounts).

Verifying typed data (EIP-712)

import { verify_typed_data } from "@ethernauta/crypto";

const ok = await verify_typed_data({
  address: signer,
  typed_data,
  signature,
})(reader({ chain_id: eip155_1.chain_id }));

Same pattern: verify_typed_data is the convenience entry, verify_typed_data_deployed and verify_typed_data_universal are the lower tiers.

Verifying SIWE (EIP-4361)

import { verify_siwe_message } from "@ethernauta/crypto";

const result = await verify_siwe_message({
  message,        // the SIWE message string
  signature,
  domain,
  nonce,
})(reader({ chain_id: eip155_1.chain_id }));

if (result.valid) {
  console.log("address:", result.address);
} else {
  console.log("reason:", result.reason);
  // ↑ VerifySiweMessageFailureReason
}

Parses + verifies + cross-checks the embedded domain, URI, chain ID, and nonce. Failure modes are typed (VerifySiweMessageFailureReason) so the rejection branch can be acted on programmatically.

Signing primitives

For when the dapp owns a private key (server-side flows, key-derived accounts, tests):

import {
  sign_digest,
  sign_typed_data,
  personal_sign_message,
  signature_to_hex,
} from "@ethernauta/crypto";

const signature = sign_digest({ digest, private_key });
const hex = signature_to_hex(signature);
HelperPurpose
sign_digestSign a 32-byte digest with an ECDSA private key.
sign_typed_dataHash + sign an EIP-712 typed-data payload.
personal_sign_messageEIP-191 personal_sign over a UTF-8 message.
signature_to_hexPack { r, s, v } into 0x + 130 hex chars.

Recovery

import { recover_address } from "@ethernauta/crypto";

const address = recover_address({ digest, signature });

recover_address is the inverse of sign_digest — given the digest that was signed and the signature, return the address whose private key signed it.

Key derivation (BIP-32 / 39 / 44)

import {
  mnemonic_to_seed,
  seed_to_master_key,
  derive_private_key,
  private_key_to_address,
  type HDKey,
} from "@ethernauta/crypto";

const seed = await mnemonic_to_seed("twelve word mnemonic ...");
const master = seed_to_master_key(seed);
const account = derive_private_key(master, "m/44'/60'/0'/0/0");
const address = private_key_to_address(account);

Used by the wallet’s vault to derive addresses from the encrypted mnemonic. Exposed in crypto because off-wallet flows (test fixtures, hardware-wallet adapters, key-rotation scripts) need the same primitives.

HDKey is re-exported from @scure/bip32.

Hashing

import { keccak_256 } from "@ethernauta/crypto";

const hash = keccak_256(bytes);  // → Uint8Array(32)

Re-export of @noble/hashes/sha3’s keccak_256. Hashing primitives don’t need their own package — they pass through here for ergonomic import.

Why this split from @ethernauta/eip/1271 / eip/6492

The @ethernauta/eip/<n>/ packages own the wire-shape definitions and the magic constants of each spec. They expose verify_hash per EIP — a method scoped to that EIP’s exact verification flow.

@ethernauta/crypto owns the cross-EIP verification — the function that knows how to fall through 191 → 1271 → 6492 in the right order for “verify this signature came from this address.” That’s a separate concern from any one EIP.

The split satisfies hard rule 11 in CLAUDE.md (numbered-standard logic stays in its numbered folder) while still giving consumers a single import for the common “just verify this” use case.

See also

  • EIP-191 — personal_sign wire format.
  • EIP-712 — typed-data wire format.
  • EIP-1271 — smart-contract signature validation.
  • EIP-6492 — counterfactual signature wrapping.
  • EIP-4361 — Sign-In with Ethereum.