Client library for Quorum blockchain with Post-Quantum Cryptography (PQC) and Zero-Knowledge Proof (ZK) support
npm install pqc-ethersClient 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.
- 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[]], ...]
``bash`
npm install pqc-ethers
- ethers@^5.7.2 — Ethereum library@noble/post-quantum@^0.4.0
- — ML-DSA (FIPS 204) for PQC signatures
`javascript
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();
`
`javascript
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');
`
`javascript
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)QuorumProvider
- createQuorumProvider(url): Returns for http(s): and QuorumWebSocketProvider for ws(s):
Standard Ethereum wallet (ECDSA).
`javascript
const { ECDSAWallet } = require('pqc-ethers');
const wallet = new ECDSAWallet('0x...', provider);
console.log(wallet.address);
const sig = await wallet.signMessage('Hello');
`
Post-quantum wallet using ML-DSA-65. Key generation is asynchronous.
`javascript
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 with keys already initialized"ML-DSA-65"
- sign(hash): Signs the 32-byte hash with context (matches Go node)
- verify(hash, signature): Verifies using the same context
- fromSecretKey(secretKey, publicKey): Build wallet from existing ML-DSA-65 keys
ECDSA + PQC; supports Legacy transactions only.
`javascript
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
Transaction type is inferred from params:
- Legacy (0): default when no accessList / no maxFeePerGasaccessList
- AccessList (1): when is present and non-empty (or type === 1)maxFeePerGas
- DynamicFee (2): when both and maxPriorityFeePerGas are set (or type === 2)
Use unified classes ECDSATransaction, PQCTransaction, or aliases: LegacyTransaction, PQCLegacyTransaction, AccessListTransaction, PQCAccessListTransaction, DynamicFeeTransaction, PQCDynamicFeeTransaction.
`javascript
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);
`
`javascript
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);
`
`javascript
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".
`javascript
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());
`
`javascript
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.
`javascript
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 (ECDSA + PQC) supports Legacy only.
`javascript
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.
`javascript
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 });
`
`javascript
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);
`
`javascript
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
`
`javascript
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...');
`
The examples/send-coin.js script demonstrates:
1. Provider tests (HTTP, WebSocket via createQuorumProvider)wallet.sendTransaction()
2. ECDSA — ECDSATransaction
3. ECDSA — Legacy (unified )
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:
`bash`
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.
- 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.await PQCWallet.create()
- Async PQC keys: Use or await wallet._initPromise before using address/sign.getGasPrice()
- BigInt vs BigNumber: For ethers v5, returns BigNumber; use BigInt(gasPriceBn.toString()) when setting gasPrice / maxFeePerGas / maxPriorityFeePerGas in transaction params.[[addressHex, storageKeysHex[]], ...]
- RLP: Integer encoding is canonical (no leading zero bytes); AccessList is encoded as for type 1/2.accessList
- Hybrid: Only Legacy (type 0) is supported; do not set or maxFeePerGas` for Hybrid.
LGPL-3.0