Quantum-resistant wallet library for The QRL using ML-DSA-87 (FIPS 204).
- ML-DSA-87 digital signatures (NIST post-quantum standard)
- Deterministic key derivation from seeds
- Mnemonic phrase backup (34 words)
- Address generation and validation
- Works in Node.js and browsers
- Dual ESM/CommonJS support
npm install @theqrl/wallet.jsimport { MLDSA87, Seed, isValidAddress } from '@theqrl/wallet.js';
// Create a new random wallet
const wallet = MLDSA87.newWallet();
console.log('Address:', wallet.getAddressStr());
console.log('Mnemonic:', wallet.getMnemonic());
// Sign a message
const message = new TextEncoder().encode('Hello QRL!');
const signature = wallet.sign(message);
// Verify signature (descriptor is required so verification uses the
// same domain-separated context as signing)
const isValid = MLDSA87.verify(signature, message, wallet.getPK(), wallet.getDescriptor());
console.log('Valid:', isValid); // true
// Clean up sensitive data
wallet.zeroize();const { MLDSA87, Seed, isValidAddress } = require('@theqrl/wallet.js');
const wallet = MLDSA87.newWallet();
console.log('Address:', wallet.getAddressStr());Creates a new wallet with a random seed.
const wallet = MLDSA87.newWallet();
const walletWithMeta = MLDSA87.newWallet([0x01, 0x02]); // Custom 2-byte metadataCreates a wallet from an existing seed (deterministic).
const seed = Seed.from('0x' + '00'.repeat(48)); // 48-byte hex string
const wallet = MLDSA87.newWalletFromSeed(seed);Restores a wallet from a mnemonic phrase.
const mnemonic = 'absorb aback veto waiter rail aroma...'; // 34 words
const wallet = MLDSA87.newWalletFromMnemonic(mnemonic);Factory function that auto-detects wallet type from extended seed.
import { newWalletFromExtendedSeed } from '@theqrl/wallet.js';
const wallet = newWalletFromExtendedSeed('0x01000000...'); // 51-byte hex| Method | Returns | Description |
|---|---|---|
getAddressStr() |
string |
Address with Q prefix (e.g., Qabc123...), 129 chars total, lowercase hex. Pass through toChecksumAddress to get the EIP-55-style mixed-case form |
getAddress() |
Uint8Array |
Raw address bytes (64 bytes, see ADDRESS_SIZE) |
getMnemonic() |
string |
34-word mnemonic phrase |
getPK() |
Uint8Array |
Public key (2,592 bytes) |
getSK() |
Uint8Array |
Secret key (4,896 bytes) |
getHexExtendedSeed() |
string |
Extended seed as hex with 0x prefix |
sign(message) |
Uint8Array |
Sign a message (4,627-byte signature). Hedged by default (FIPS 204 §3.4, recommended per TOB-QRLLIB-6) — two signs of the same message produce distinct bytes that both verify under the same pk + descriptor |
signDeterministic(message) |
Uint8Array |
Sign deterministically (FIPS 204 §3.5). Use only when determinism is itself a protocol requirement (RANDAO-style beacon contributions, KAT / ACVP vector reproduction). See SECURITY.md |
zeroize() |
void |
Overwrite sensitive data with zeros |
| Method | Description |
|---|---|
MLDSA87.verify(signature, message, pk, descriptor) |
Verify a signature, returns boolean. The descriptor is required so verification uses the same domain-separated context as signing |
Every wallet-level signature is bound to its descriptor via an 8-byte domain-separated context:
"ZOND" || SIGNING_CONTEXT_VERSION || descriptor (4 + 1 + 3 bytes)
ML-DSA-87 passes this as the FIPS 204 ctx parameter. Callers do not usually need to construct it — wallet.sign(message) and MLDSA87.verify(sig, msg, pk, descriptor) do it internally — but the helper is exported for advanced callers and cross-implementation parity with go-qrllib:
import { signingContext, SIGNING_CONTEXT_VERSION } from '@theqrl/wallet.js';
const ctx = signingContext(wallet.getDescriptor()); // Uint8Array(8)Bumping SIGNING_CONTEXT_VERSION is a hard break of the signature wire format: signatures produced under a new version will not verify under the old one. A version bump must coincide with a coordinated consensus / library activation.
Address Format: Q prefix followed by exactly 128 hex characters (64-byte address, NIST Category 5). The 64-byte size matches go-qrllib's AddressSize and rust-qrllib's ADDRESS_SIZE — there is one canonical address size across all QRL implementations.
addressToStringemits lowercase hex;toChecksumAddressemits the EIP-55-style mixed-case checksummed form- The
Qprefix is always uppercase on output; input parsing acceptsQorq addressToString,stringToAddress,toChecksumAddress,isValidAddress, andisValidChecksumAddressenforce the exact 64-byte (128-hex-char) length and reject anything else
stringToAddress and isValidAddress accept three encodings of the same address:
- all-lowercase hex (legacy / case-uniform),
- all-uppercase hex (legacy / case-uniform), and
- mixed-case hex that satisfies the checksum.
Mixed-case strings whose case does not match the checksum are rejected, mirroring how Ethereum tooling treats EIP-55 addresses. This means a single mistyped character in a checksummed address is detected on parse.
The scheme follows EIP-55 with one substitution: the hash is SHAKE-256 of the UTF-8 bytes of the 128-character lowercase hex (no Q prefix), with dkLen = ADDRESS_SIZE, giving exactly one hash nibble per hex character. For each hex character, if it is a letter (a-f) and the corresponding nibble is ≥ 8, it is uppercased; otherwise it stays lowercase. The Q prefix is not part of the checksum input.
isValidChecksumAddress is strict: it returns true only when the input exactly matches the canonical checksummed form produced by toChecksumAddress (uppercase Q, hex body case-for-case identical). All-lowercase and all-uppercase forms that contain letters return false. Use it for "did the caller paste a checksummed address?" — and use isValidAddress for the permissive parse check.
import {
addressToString,
stringToAddress,
toChecksumAddress,
isValidAddress,
isValidChecksumAddress,
} from '@theqrl/wallet.js';
// Convert bytes to string (lowercase)
const addrStr = addressToString(addressBytes); // 'Qabc...'
// Convert bytes (or any case-form string) to the checksummed mixed-case form
const checksummed = toChecksumAddress(addressBytes); // 'QAbC...' (mixed case)
// Parse: accepts lowercase, uppercase, or correctly-checksummed mixed case
const a = stringToAddress('Qabc...'); // ok
const b = stringToAddress('QABC...'); // ok
const c = stringToAddress(checksummed); // ok
// stringToAddress('QAbc...') // throws if checksum is wrong
// Permissive check (accepts any of the three forms above)
if (isValidAddress(userInput)) { /* ... */ }
// Strict check (only true for the canonical checksummed form)
if (isValidChecksumAddress(userInput)) { /* ... */ }import {
Seed,
ExtendedSeed,
Descriptor,
newMLDSA87Descriptor,
SEED_SIZE, // 48
EXTENDED_SEED_SIZE, // 51
DESCRIPTOR_SIZE // 3
} from '@theqrl/wallet.js';
// Create seed from hex
const seed = Seed.from('0x' + 'ab'.repeat(48));
// Create descriptor
const descriptor = newMLDSA87Descriptor([0x00, 0x00]);
// Create extended seed
const extSeed = ExtendedSeed.newExtendedSeed(descriptor, seed);See SECURITY.md for the security model and best practices.
Important:
- Always call
wallet.zeroize()when done - Never log or transmit mnemonics/seeds
- Mnemonics do not include a built-in checksum — application-layer verification is recommended (see SECURITY.md for details)
- Validate addresses with
isValidAddress()before use (accepts uniform-case and checksummed forms), or withisValidChecksumAddress()to require an EIP-55-style checksummed address
The library works in browsers via bundlers (webpack, vite, etc.):
import { MLDSA87 } from '@theqrl/wallet.js';
const wallet = MLDSA87.newWallet();Uses Web Crypto API for secure random number generation (see Requirements).
This library currently supports ML-DSA-87 (FIPS 204), the NIST standardized version of Dilithium.
| Property | Value |
|---|---|
| Security Level | NIST Level 5 |
| Public Key | 2,592 bytes |
| Secret Key | 4,896 bytes |
| Signature | 4,627 bytes |
@theqrl/mldsa87- ML-DSA-87 implementation@noble/hashes- SHA-256, SHAKE-256
- Node.js: 20.19+, 22.x, or 24.x (requires
globalThis.crypto.getRandomValues) - Browsers: Any modern browser with Web Crypto API support (
crypto.getRandomValues()) and ES2020 (BigInt). This includes Chrome 67+, Firefox 68+, Safari 14+, and Edge 79+. - Not supported: Internet Explorer, Node.js < 20, or environments without Web Crypto API