npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pq-ethers

v0.1.0

Published

ethers for the post-quantum era: drop-in ethers v6 API for quantum-resistant EVM chains. Wallets, transactions, contracts and EIP-191/712 signing with ML-DSA-87 (FIPS 204 / CRYSTALS-Dilithium) instead of ECDSA.

Readme

pq-ethers

npm version License: MIT TypeScript

ethers for the post-quantum era. Same API you already know; every signature is ML-DSA-87 (FIPS 204, aka CRYSTALS-Dilithium) instead of ECDSA.

Quantum computers will eventually break secp256k1, the curve every ECDSA chain and wallet depends on. Natively post-quantum EVM chains already exist today, and they sign transactions with ML-DSA, the NIST-standardized lattice signature scheme. That breaks the entire existing tool stack: MetaMask, ethers.Wallet, web3.js, and every ECDSA signer simply cannot sign for these chains.

pq-ethers is a drop-in, ethers-v6-shaped client for quantum-resistant EVM chains: create wallets, derive accounts from a mnemonic, sign and send transactions, deploy and call contracts, sign EIP-191/EIP-712 messages, with the same shapes ethers gives you. Everything ML-DSA doesn't change (reads, ABI encoding, events, utils) is the real ethers, re-exported, so you install one package.

npm install pq-ethers     # ethers comes with it; you don't install it separately

Why pq-ethers

| | What it is | Why it's not enough on its own | | ---------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | @noble/post-quantum | The ML-DSA math: sign(hash, key) -> bytes | No addresses, transactions, nonces, gas, RPC, or contracts. pq-ethers is the chain client built on top of it, the same way ethers sits on @noble/curves. | | ethers / web3.js | The EVM chain client everyone knows | ECDSA only. Cannot sign for a chain whose transactions carry ML-DSA signatures. | | Per-chain web3 forks (e.g. @theqrl/web3) | A full web3.js fork adapted for one PQ chain | Forking the whole client per chain doesn't scale. In pq-ethers a chain's envelope is a one-file plugin (SigScheme), not a fork; the wallet, HD, keystore, and contract layers are shared. | | Ethereum's own PQ roadmap (EIP-8051 precompile, account abstraction) | PQ signatures verified inside smart-contract wallets on ECDSA chains | Different architecture. It doesn't help chains where ML-DSA is the transaction signature. pq-ethers targets those natively post-quantum chains. |

Highlights

  • Familiar DX: Wallet extends ethers' AbstractSigner, so it works anywhere an ethers Signer is accepted.
  • NIST-standard crypto, zero hand-rolled: ML-DSA-87 via audited-family @noble/post-quantum, hashing via @noble/hashes, hedged (randomized) signing per FIPS 204.
  • Full account stack: BIP-39 mnemonics, deterministic ML-DSA HD derivation, encrypted keystore (PBKDF2 + AES-256-GCM), raw key import/export.
  • Contracts: deploy, read, and write, including the PQ-safe write path that ethers' own contract plumbing can't provide (see below).
  • EIP-191 / EIP-712 signing with standard digests, so dapps hash exactly as they already do.
  • Chain-pluggable by design: the per-chain envelope (address derivation, sig-hash, raw-tx layout) is an interface with a registry. Supporting a new PQ chain is one file, in or out of tree.
  • Isomorphic: pure TypeScript, no WASM, runs in Node 20.19+ and browsers.

Quick start

import { Wallet, JsonRpcProvider, Contract, deployContract } from 'pq-ethers';

const provider = new JsonRpcProvider(process.env.RPC_URL);
const wallet = new Wallet(process.env.MLDSA_PRIVATE_KEY, provider); // 9792 hex chars

// send value
const tx = await wallet.sendTransaction({ to: '0x…', value: 10n ** 18n });
await tx.wait();

// deploy
const { address, contract } = await deployContract(abi, bytecode, [arg1, arg2], wallet);

// read (plain ethers under the hood - unchanged by ML-DSA)
const c = new Contract(address, abi, provider);
const v = await c.someView();

Accounts: create, derive, import

import { Wallet, HDWallet } from 'pq-ethers';

// new wallet with a recoverable 24-word phrase
const w = Wallet.createRandom(provider);
console.log(w.mnemonic, w.address);

// derive multiple accounts from one phrase (ML-DSA HD)
const hd = HDWallet.fromPhrase(w.mnemonic!);
const a0 = hd.deriveAccount(0);
const a1 = hd.deriveAccount(1);

// encrypted keystore (AES-256-GCM) round-trip
const json = await w.encrypt('password');
const loaded = await Wallet.fromEncryptedJson(json, 'password', provider);

// import a raw ML-DSA private key (9792 hex chars)
const imported = new Wallet(process.env.MLDSA_PRIVATE_KEY, provider);

ML-DSA keys aren't secp256k1 scalars, so BIP-32/44 paths don't apply. The HD scheme is HMAC-SHA512(bip39Seed, "mldsa87/account/<i>")[:32] -> ML-DSA keygen; standard BIP-39 in front, domain-separated derivation behind. See docs/architecture.md for the full rationale.

Why post-quantum signing changes the client (30 seconds)

Two properties of ML-DSA drive everything in this library:

  1. No key recovery. ECDSA lets the chain recover your public key from a signature (ecrecover); ML-DSA mathematically cannot. So every PQ transaction and off-chain signature must carry its public key, and verification returns address-or-null instead of recovering a signer.
  2. Big signatures. An ML-DSA-87 signature is ~4.6 KB and the public key ~2.6 KB (vs 65 bytes total for ECDSA). ethers' transaction parser rejects them, which is why writes must go through pq-ethers' own send path:
import { writeContract } from 'pq-ethers';

// ✅ PQ-safe write: signs and broadcasts through the Wallet
await writeContract(contract, 'transfer', [to, amount]);

// ❌ breaks on PQ chains: ethers wraps this in ContractTransactionResponse,
//    whose .wait() parses the signature as 65-byte ECDSA and throws
await contract.transfer(to, amount);

Reads are untouched: Contract calls, events, and ABI work exactly as in ethers.

One library, many chains: pluggable schemes

ML-DSA is a universal standard, but each chain invents its own envelope: address derivation, signing hash, raw-tx layout. That envelope is the only chain-specific part of pq-ethers, isolated behind the SigScheme interface. Built-in schemes:

| Scheme | Sig-hash | Use when | | ---------------------------------- | ----------------------------------- | ---------------------------------------------------------- | | mldsa87Keccak512Scheme (default) | keccak512(keccak256(rlp(...))) | the target chain verifies against the double-hash envelope | | mldsa87Keccak256Scheme | keccak256(rlp(...)) (single pass) | the target chain verifies a plain EIP-155-style RLP hash |

You select a scheme by passing the scheme object - imported like any other value, type-checked, and tree-shaken if unused:

import { Wallet, mldsa87Keccak256Scheme } from 'pq-ethers';
const wallet = new Wallet(key, provider, mldsa87Keccak256Scheme);

Knowing which envelope your chain verifies is your responsibility - the two shipped schemes produce different signing-hashes for the same chain ID, and there is no way to detect from the client side which one a node expects. Pick the one that matches your chain's Sender() implementation and confirm with one real transaction. This is the permanent contract: pq-ethers will not guess or auto-negotiate the envelope.

Add your chain without forking

SigScheme is public, so a new chain (or a new PQ algorithm, e.g. SLH-DSA) is implemented entirely from your own repo:

  1. Implement SigScheme (name, secretKeyLength, getPublicKey, deriveAddress, computeCreateAddress, signTransaction). Use src/tx/schemes/mldsa87-keccak512.ts as the reference.
  2. Pass it wherever a scheme is accepted: new Wallet(key, provider, myScheme), HDWallet.fromPhrase(phrase, { scheme: myScheme }), etc.
  3. (Optional) If you need to resolve schemes from a runtime string - a name read from config, or decrypting keystores that reference your scheme by name - call registerScheme(myScheme) once at startup and resolve with getScheme(name). Built-in scheme names resolve out of the box (e.g. getScheme(process.env.MLDSA_TX_SCHEME || 'mldsa87-keccak512')); only custom schemes need registering. Note: importing getScheme bundles both built-in schemes (it must be able to return either at runtime) - passing the object stays fully tree-shakeable.

Wallet, HD, keystore, contracts, and RPC all consume SigScheme generically and keep working unchanged. This is the difference from the fork-per-chain approach: the framework is shared; only the envelope is per-chain.

FAQ

Won't Ethereum's own quantum upgrade make this unnecessary? Ethereum's path (ML-DSA precompiles + account abstraction) verifies PQ signatures inside smart-contract wallets while transactions stay ECDSA-compatible. pq-ethers is for chains where ML-DSA is the native transaction signature today. Both worlds will coexist, and pq-ethers re-exports ethers so dual-chain tooling needs only one dependency.

Is the cryptography audited? pq-ethers adds zero cryptography: signatures come from @noble/post-quantum and hashing from @noble/hashes (the same noble family ethers and viem build on). pq-ethers itself, the chain-integration layer, has not yet been independently audited; review the ~1.1k lines of src/ yourself, they are written to be read.

Can I verify a pq-ethers transaction independently? Yes. scripts/live-check.mjs signs against a live node and re-verifies the embedded ML-DSA signature, and the scheme tests decode every raw tx and verify the signature against an independently recomputed sig-hash.

Why is my raw transaction ~10 KB? That's ML-DSA: ~4.6 KB signature + ~2.6 KB public key in every transaction (the pubkey must travel with the tx because there is no key recovery). It's the inherent cost of lattice signatures, not overhead added by this library.

Documentation

  • docs/api-reference.md: every exported function/class/type, with full signatures, defaults, and error messages. Read this instead of src/ when integrating.
  • docs/architecture.md: the rationale: why noble, why a pluggable scheme, why the envelope looks the way it does, and what a new chain needs.

Develop

npm install
npm test          # vitest
npm run build     # tsup -> dist (ESM + CJS + d.ts)
npm run typecheck

Security

  • All ML-DSA-87 operations come from @noble/post-quantum; hashing from @noble/hashes. No hand-rolled cryptography anywhere in this package.
  • Signing is hedged (fresh 32-byte entropy per signature, FIPS 204 randomized variant).
  • Keystore: PBKDF2-SHA256 (600k iterations) + AES-256-GCM via WebCrypto.
  • Transaction serialization is exercised against a live chain node via scripts/live-check.mjs and verified structurally in the scheme tests.

To report a vulnerability, please use private disclosure rather than a public issue.

License

MIT