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); | Helper | Purpose |
|---|---|
sign_digest | Sign a 32-byte digest with an ECDSA private key. |
sign_typed_data | Hash + sign an EIP-712 typed-data payload. |
personal_sign_message | EIP-191 personal_sign over a UTF-8 message. |
signature_to_hex | Pack { 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.