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

pqc-ethers

v1.0.1

Published

Client library for Quorum blockchain with Post-Quantum Cryptography (PQC) and Zero-Knowledge Proof (ZK) support

Readme

pqc-ethers

Client library for Quorum blockchain with Post-Quantum Cryptography (PQC) and Zero-Knowledge Proof (ZK) support. Uses ethers v5 and @noble/post-quantum (ML-DSA-65 / FIPS 204) for PQC signing.

Features

  • Post-Quantum Cryptography: ML-DSA-65 (Dilithium) via @noble/post-quantum; signing uses context "ML-DSA-65" to match Quorum Go node verification
  • Hybrid Signatures: ECDSA + PQC on the same transaction (Legacy only)
  • Unified transaction classes: ECDSATransaction, PQCTransaction, HybridTransaction — transaction type (Legacy / AccessList / DynamicFee) is inferred from fields (accessList, maxFeePerGas / maxPriorityFeePerGas)
  • Transaction types: Legacy (0), AccessList / EIP-2930 (1), DynamicFee / EIP-1559 (2)
  • Providers: HTTP (QuorumProvider) and WebSocket (QuorumWebSocketProvider); createQuorumProvider(url) picks by URL scheme
  • ERC20: Simplified interface for token transfers (ECDSA, PQC, Hybrid)
  • ZK: Support for ZK transactions (Groth16, PLONK, STARK)
  • RLP: Canonical RLP encoding compatible with Go Quorum node; AccessList encoded as [[addressHex, storageKeysHex[]], ...]

Installation

npm install pqc-ethers

Dependencies

  • ethers@^5.7.2 — Ethereum library
  • @noble/post-quantum@^0.4.0 — ML-DSA (FIPS 204) for PQC signatures

Quick Start

CommonJS

const { QuorumProvider, ECDSAWallet, PQCWallet, HybridWallet, createQuorumProvider } = require('pqc-ethers');

const provider = new QuorumProvider('http://localhost:8545');

const ecdsaWallet = new ECDSAWallet('0x...', provider);

const pqcWallet = await PQCWallet.create();

const hybridWallet = await HybridWallet.create(ecdsaWallet);
const hybridAddress = await hybridWallet.getAddress();

ES Modules

import { QuorumProvider, ECDSAWallet, PQCWallet, createQuorumProvider } from 'pqc-ethers';

const provider = new QuorumProvider('http://localhost:8545');
const pqcWallet = await PQCWallet.create();
const wsProvider = createQuorumProvider('ws://localhost:8546');

Providers

HTTP and WebSocket

const { QuorumProvider, QuorumWebSocketProvider, createQuorumProvider } = require('pqc-ethers');

const httpProvider = new QuorumProvider('http://localhost:8545');
const wsProvider = createQuorumProvider('ws://localhost:8546');

if (wsProvider instanceof QuorumWebSocketProvider) {
  const block = await wsProvider.getBlockNumber();
  await wsProvider.destroy();
}
  • QuorumProvider: HTTP RPC
  • QuorumWebSocketProvider: WebSocket RPC (subclass of ethers.providers.WebSocketProvider)
  • createQuorumProvider(url): Returns QuorumProvider for http(s): and QuorumWebSocketProvider for ws(s):

Wallets

ECDSA Wallet

Standard Ethereum wallet (ECDSA).

const { ECDSAWallet } = require('pqc-ethers');

const wallet = new ECDSAWallet('0x...', provider);
console.log(wallet.address);
const sig = await wallet.signMessage('Hello');

PQC Wallet

Post-quantum wallet using ML-DSA-65. Key generation is asynchronous.

const { PQCWallet } = require('pqc-ethers');

const wallet = await PQCWallet.create();
const address = await wallet.getAddress();

const hash = new Uint8Array(32);
const signature = await wallet.sign(hash);
const ok = await wallet.verify(hash, signature);

const walletFromKeys = PQCWallet.fromSecretKey(secretKey, publicKey);
  • PQCWallet.create(): Returns Promise<PQCWallet> with keys already initialized
  • sign(hash): Signs the 32-byte hash with context "ML-DSA-65" (matches Go node)
  • verify(hash, signature): Verifies using the same context
  • fromSecretKey(secretKey, publicKey): Build wallet from existing ML-DSA-65 keys

Hybrid Wallet

ECDSA + PQC; supports Legacy transactions only.

const { HybridWallet, ECDSAWallet, PQCWallet } = require('pqc-ethers');

const ecdsaWallet = new ECDSAWallet('0x...');
const hybridWallet = await HybridWallet.create(ecdsaWallet);
const address = await hybridWallet.getAddress();

const hybridFromKeys = HybridWallet.fromKeys(ecdsaPrivateKey, pqcSecretKey, pqcPublicKey);
  • HybridWallet.create(ecdsaWallet): Accepts ECDSAWallet or ECDSA private key string; generates PQC keys internally

Transactions

Transaction type is inferred from params:

  • Legacy (0): default when no accessList / no maxFeePerGas
  • AccessList (1): when accessList is present and non-empty (or type === 1)
  • DynamicFee (2): when both maxFeePerGas and maxPriorityFeePerGas are set (or type === 2)

Use unified classes ECDSATransaction, PQCTransaction, or aliases: LegacyTransaction, PQCLegacyTransaction, AccessListTransaction, PQCAccessListTransaction, DynamicFeeTransaction, PQCDynamicFeeTransaction.

ECDSA — Legacy (Type 0)

const { ECDSATransaction, LegacyTransaction, TX_TYPE, ethers } = require('pqc-ethers');

const tx = new LegacyTransaction({
  chainId: 1337,
  nonce: 0,
  gasPrice: await provider.getGasPrice(),
  gasLimit: 21000n,
  to: '0x...',
  value: ethers.utils.parseEther('1.0'),
});
await tx.sign(ecdsaWallet);
const hex = tx.getTxType() === TX_TYPE.LEGACY ? ethers.utils.hexlify(tx.serialize()) : tx.signedHex;
const txHash = await provider.sendRawTransaction(hex);

ECDSA — AccessList (Type 1)

const { ECDSATransaction, TX_TYPE, ethers } = require('pqc-ethers');

const gasPriceBn = await provider.getGasPrice();
const tx = new ECDSATransaction({
  chainId: 1337,
  nonce: await provider.getTransactionCount(ecdsaWallet.address, 'pending'),
  gasPrice: BigInt(gasPriceBn.toString()),
  gasLimit: 21000n,
  to: '0x...',
  value: ethers.utils.parseEther('0.001'),
  accessList: [{ address: '0x...', storageKeys: [] }],
});
await tx.sign(ecdsaWallet);
const hex = tx.getTxType() === TX_TYPE.LEGACY ? ethers.utils.hexlify(tx.serialize()) : tx.signedHex;
await provider.sendRawTransaction(hex);

ECDSA — DynamicFee (Type 2, EIP-1559)

const { ECDSATransaction, TX_TYPE, ethers } = require('pqc-ethers');

const gasPriceBn = await provider.getGasPrice();
const gasPrice = BigInt(gasPriceBn.toString());
const tx = new ECDSATransaction({
  chainId: 1337,
  nonce: await provider.getTransactionCount(ecdsaWallet.address, 'pending'),
  maxPriorityFeePerGas: gasPrice,
  maxFeePerGas: gasPrice * 2n,
  gasLimit: 21000n,
  to: '0x...',
  value: ethers.utils.parseEther('0.001'),
});
await tx.sign(ecdsaWallet);
const hex = tx.getTxType() === TX_TYPE.LEGACY ? ethers.utils.hexlify(tx.serialize()) : tx.signedHex;
await provider.sendRawTransaction(hex);

Note: In ethers v5, provider.getGasPrice() returns a BigNumber. When passing into fields used as BigInt (e.g. maxFeePerGas), use BigInt(gasPriceBn.toString()) to avoid "Cannot mix BigInt and other types".

PQC — Legacy (Type 0)

const { PQCTransaction, PQCLegacyTransaction, ethers } = require('pqc-ethers');

const nonce = await provider.getTransactionCount(await pqcWallet.getAddress(), 'pending');
const gasPriceBn = await provider.getGasPrice();
const tx = new PQCLegacyTransaction({
  chainId: 1337,
  nonce,
  gasPrice: BigInt(gasPriceBn.toString()),
  gasLimit: 21000n,
  to: '0x...',
  value: ethers.utils.parseEther('0.001'),
});
await tx.sign(pqcWallet);
const txHash = await provider.sendRawTransaction(tx.getHex());

PQC — AccessList (Type 1)

const { PQCTransaction, ethers } = require('pqc-ethers');

const tx = new PQCTransaction({
  chainId: 1337,
  nonce: await provider.getTransactionCount(await pqcWallet.getAddress(), 'pending'),
  gasPrice: BigInt((await provider.getGasPrice()).toString()),
  gasLimit: 21000n,
  to: '0x...',
  value: ethers.utils.parseEther('0.001'),
  accessList: [{ address: '0x...', storageKeys: [] }],
});
await tx.sign(pqcWallet);
await provider.sendRawTransaction(tx.getHex());

AccessList must be an array of { address, storageKeys }. The library converts it to RLP-encodable form internally.

PQC — DynamicFee (Type 2)

const { PQCTransaction, ethers } = require('pqc-ethers');

const gasPriceBn = await provider.getGasPrice();
const gasPrice = BigInt(gasPriceBn.toString());
const tx = new PQCTransaction({
  chainId: 1337,
  nonce: await provider.getTransactionCount(await pqcWallet.getAddress(), 'pending'),
  maxPriorityFeePerGas: gasPrice,
  maxFeePerGas: gasPrice * 2n,
  gasLimit: 21000n,
  to: '0x...',
  value: ethers.utils.parseEther('0.001'),
});
await tx.sign(pqcWallet);
await provider.sendRawTransaction(tx.getHex());

Hybrid — Legacy only (Type 0)

Hybrid (ECDSA + PQC) supports Legacy only.

const { HybridTransaction, HybridLegacyTransaction, ethers } = require('pqc-ethers');

const tx = new HybridLegacyTransaction({
  chainId: 1337,
  nonce: await provider.getTransactionCount(await hybridWallet.getAddress(), 'pending'),
  gasPrice: BigInt((await provider.getGasPrice()).toString()),
  gasLimit: 21000n,
  to: '0x...',
  value: ethers.utils.parseEther('0.001'),
});
await tx.sign(hybridWallet);
await provider.sendRawTransaction(tx.getHex());

Signing uses the same hash and ML-DSA-65 context as the Go node so verification succeeds.

ERC20 Tokens

const { ERC20Token } = require('pqc-ethers');

const token = new ERC20Token(tokenAddress, provider, ecdsaWallet);
const tx = await token.transfer(recipientAddress, amount, { gasPrice, gasLimit });

const txHashPQC = await token.transferPQC(pqcWallet, provider, recipientAddress, amount, { chainId, gasPrice: 0n, gasLimit });
const txHashHybrid = await token.transferHybrid(hybridWallet, provider, recipientAddress, amount, { chainId, gasPrice: 0n, gasLimit });

Zero-Knowledge Transactions

const { ZKTransaction, ZK_PROOF_SYSTEM } = require('pqc-ethers');

const zkTx = new ZKTransaction({
  chainId: 1337,
  nonce: 0,
  gasPrice: 0n,
  gasLimit: 21000,
  to: '0x...',
  value: ethers.utils.parseEther('1.0'),
  zkProofSystem: ZK_PROOF_SYSTEM.GROTH16,
  zkProof: proofBytes,
  zkPublicInputs: publicInputs,
  zkVerificationKeyHash: vkHash,
});
const txHash = await zkTx.send(provider, senderAddress);

Constants

const {
  TX_TYPE,
  PQC_TYPE,
  ZK_PROOF_SYSTEM,
  DEFAULT_CHAIN_ID,
} = require('pqc-ethers');

TX_TYPE.LEGACY        // 0
TX_TYPE.ACCESS_LIST   // 1
TX_TYPE.DYNAMIC_FEE   // 2
TX_TYPE.ZK_PRIVATE    // 4

PQC_TYPE.NONE         // 0
PQC_TYPE.DILITHIUM    // 1

ZK_PROOF_SYSTEM.GROTH16  // 1
ZK_PROOF_SYSTEM.PLONK    // 2
ZK_PROOF_SYSTEM.STARK    // 3

DEFAULT_CHAIN_ID      // 1337

Utilities

const {
  derivePQCAddress,
  deriveHybridAddress,
  isValidAddress,
  encodeUint64,
  encodeBigInt,
  encodeSignature,
  ethers,
} = require('pqc-ethers');

const address = derivePQCAddress(publicKey);
const hybridAddress = deriveHybridAddress(ecdsaPublicKey, pqcPublicKey);
const ok = isValidAddress('0x...');
const nonceHex = encodeUint64(5);
const valueHex = encodeBigInt(ethers.utils.parseEther('1.0'));
const sigHex = encodeSignature('0x...');

Example script

The examples/send-coin.js script demonstrates:

  1. Provider tests (HTTP, WebSocket via createQuorumProvider)
  2. ECDSA — wallet.sendTransaction()
  3. ECDSA — Legacy (unified ECDSATransaction)
  4. PQC — Legacy
  5. Hybrid — Legacy
  6. ECDSA — AccessList (type 1)
  7. ECDSA — DynamicFee (type 2)
  8. PQC — AccessList (type 1)
  9. PQC — DynamicFee (type 2)
  10. WebSocket send

Run with:

RPC_URL=http://your-node:8545 node examples/send-coin.js

Optional env: RPC_URL, WS_URL, CHAIN_ID, ECDSA_PRIVATE_KEY, RECIPIENT, AMOUNT, RPC_TIMEOUT_MS.

Notes

  • PQC algorithm: ML-DSA-65 (FIPS 204) via @noble/post-quantum; context "ML-DSA-65" is used for sign/verify so that client signatures verify on the Quorum Go node.
  • Async PQC keys: Use await PQCWallet.create() or await wallet._initPromise before using address/sign.
  • BigInt vs BigNumber: For ethers v5, getGasPrice() returns BigNumber; use BigInt(gasPriceBn.toString()) when setting gasPrice / maxFeePerGas / maxPriorityFeePerGas in transaction params.
  • RLP: Integer encoding is canonical (no leading zero bytes); AccessList is encoded as [[addressHex, storageKeysHex[]], ...] for type 1/2.
  • Hybrid: Only Legacy (type 0) is supported; do not set accessList or maxFeePerGas for Hybrid.

License

LGPL-3.0