mirror of
https://github.com/luxfi/wallet.git
synced 2026-07-27 03:37:41 +00:00
wallet/pq: concrete ML-DSA / ML-KEM / SLH-DSA + hybrid + HD + precompile encoders
Wires the canonical post-quantum stack per LP-4200 into the wallet:
• mldsa.ts — ML-DSA-44/65/87 providers (FIPS 204) over @noble/post-quantum
• mlkem.ts — ML-KEM-512/768/1024 KEM providers (FIPS 203)
• slhdsa.ts — SLH-DSA-128f/192f/192s/256f providers (FIPS 205)
• hybrid.ts — secp256k1∥ML-DSA + Ed25519∥ML-DSA containers; wire format
byte-compatible with luxfi/sdk wallet/keychain PQSigner
• hdPq.ts — BIP-32 → cSHAKE expandChildSeed → ML-DSA/SLH-DSA keypair
• domain.ts — FIPS-204 §5.4 ctx strings:
lux-evm-precompile-mldsa-v1 (0x012202)
lux-x-chain-utxo-v1
lux-p-chain-platform-mldsa-v1
lux-recovery-slhdsa-v1 (0x012203)
lux-handshake-mlkem-v1
• precompile.ts — eth_call encoders for 0x012201/02/03 LP-4200 verifiers
The pkgs/wallet/pq surface decomplects four concerns that were previously
tangled in luxfi/sdk's PQSigner: scheme dispatch (providerFor), domain
separation (contextBytesFor), hybrid composition (encode/decodeHybrid),
HD derivation (derivePQIdentity). Each module is independently complete;
the index re-exports them as a single PQ public API.
Web facade lives at apps/web/src/lib/pq.ts; usePQIdentity() hook derives
the canonical ML-DSA-65 identity at m/44'/9000'/<chainID>'/0'/0'/0' from
the unlocked auth-slice mnemonic.
Tests: 71 passing across 8 suites
domain: 4 mldsa: 11 mlkem: 7 slhdsa: 6
hybrid: 9 hdPq: 9 precompile: 8 pqAccount: 17 (pre-existing)
Test runner: jest.pq.config.js (standalone — bypasses upstream-shaped
jest-expo preset still pending refactor; see LLM.md).
Domain separators pinned by domain.test.ts; cSHAKE customizations pinned
by pqAccount.test.ts (pre-existing). Replay-across-chains explicitly
tested via mldsa.test.ts and hybrid.test.ts cross-ctx verify cases.
This commit is contained in:
@@ -150,6 +150,29 @@ The web SPA builds because Vite tree-shakes — only used surface is touched.
|
||||
7. **ALWAYS** preserve BIP44 path 9000 for Lux P/X chain addresses in any
|
||||
key derivation (60 for EVM C-chain).
|
||||
8. **ALWAYS** keep the GPL-3.0-or-later license header — wallet inherits it.
|
||||
9. **ALWAYS** route PQ crypto through `pkgs/wallet/src/features/wallet/pq/` —
|
||||
ML-DSA / ML-KEM / SLH-DSA / hybrid / HD-PQ derivation / domain separators /
|
||||
precompile encoders all live there. Re-export from `apps/web/src/lib/pq.ts`.
|
||||
|
||||
## PQ stack (LP-4200, FIPS 203/204/205)
|
||||
|
||||
Canonical entry: `pkgs/wallet/src/features/wallet/pq/index.ts`. Web facade:
|
||||
`apps/web/src/lib/pq.ts`. React hook: `apps/web/src/hooks/usePQIdentity.ts`.
|
||||
|
||||
| Module | Purpose |
|
||||
|-----------------|--------------------------------------------------------------|
|
||||
| `pqAccount.ts` | shape, cSHAKE AccountID, MLDSAProvider iface, HD-path strings |
|
||||
| `domain.ts` | FIPS-204 ctx strings (evm-precompile, x-chain-utxo, …) |
|
||||
| `mldsa.ts` | ML-DSA-44/65/87 providers via @noble/post-quantum |
|
||||
| `mlkem.ts` | ML-KEM-512/768/1024 KEM providers |
|
||||
| `slhdsa.ts` | SLH-DSA-128f/192f/192s/256f providers |
|
||||
| `hybrid.ts` | Ed25519∥ML-DSA + secp256k1∥ML-DSA containers (luxfi/sdk-compat)|
|
||||
| `hdPq.ts` | BIP32 → expandChildSeed → ML-DSA/SLH-DSA keypair |
|
||||
| `precompile.ts` | eth_call wrappers for 0x012201/02/03 |
|
||||
|
||||
Tests run via `pnpm --dir pkgs/wallet exec jest --config jest.pq.config.js`
|
||||
(standalone — bypasses the upstream-shaped jest-expo preset that's still
|
||||
pending refactor). 71 tests passing as of 2026-05-18.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@luxfi/wallet-brand": "workspace:^",
|
||||
"@noble/curves": "^1.6.0",
|
||||
"@noble/hashes": "^1.7.1",
|
||||
"@noble/post-quantum": "0.6.1",
|
||||
"@scure/bip32": "^1.5.0",
|
||||
"@scure/bip39": "^1.4.0",
|
||||
"@solana/web3.js": "^1.92.0",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* usePQIdentity — derive the wallet's canonical PQ identity from the
|
||||
* unlocked mnemonic.
|
||||
*
|
||||
* The mnemonic comes from the auth slice. While the wallet is locked
|
||||
* (no in-memory mnemonic) the hook returns `null`. When unlocked, it
|
||||
* computes (or returns a cached) ML-DSA-65 keypair for the current
|
||||
* chain at HD path `m/44'/9000'/<chainID>'/0'/0'/0'`.
|
||||
*
|
||||
* The cache key is `(mnemonic + chainID + accountIdx)`. We deliberately
|
||||
* cache by mnemonic content because the deterministic derivation
|
||||
* pipeline takes ~80-150 ms in JS — too slow to recompute on every
|
||||
* render. The cache is cleared on lock via the auth-slice subscription.
|
||||
*
|
||||
* Security: the cached ML-DSA-65 private key lives in module memory.
|
||||
* That's the same place the mnemonic itself sits, so we're not widening
|
||||
* the threat surface. The cache map is keyed on the mnemonic string
|
||||
* directly — JS doesn't let us pin secrets, so we accept the
|
||||
* weak-pinning trade-off in exchange for not re-running the derivation
|
||||
* 60 times per second.
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useAuth, type AuthState } from "../store/auth"
|
||||
import { type PQAccount, derivePQIdentity, WalletSchemeID } from "../lib/pq"
|
||||
|
||||
interface CacheEntry {
|
||||
mnemonic: string
|
||||
chainID: number
|
||||
accountIdx: number
|
||||
scheme: WalletSchemeID
|
||||
account: PQAccount
|
||||
}
|
||||
|
||||
let cache: CacheEntry | null = null
|
||||
|
||||
/** Drop the cached identity. Called on lock from a subscription below. */
|
||||
export function clearPQIdentityCache(): void {
|
||||
cache = null
|
||||
}
|
||||
|
||||
export function usePQIdentity(opts: {
|
||||
chainID: number
|
||||
accountIdx?: number
|
||||
scheme?: WalletSchemeID
|
||||
}): PQAccount | null {
|
||||
const accountIdx = opts.accountIdx ?? 0
|
||||
const scheme = opts.scheme ?? WalletSchemeID.MLDSA65
|
||||
|
||||
const mnemonic = useAuth((s: AuthState) => s.mnemonic)
|
||||
const isUnlocked = useAuth((s: AuthState) => s.isUnlocked)
|
||||
const [tick, setTick] = useState(0)
|
||||
|
||||
// Clear the module cache on lock. Subscribed once per hook mount.
|
||||
useEffect(() => {
|
||||
const unsub = useAuth.subscribe((s, prev) => {
|
||||
if (prev.isUnlocked && !s.isUnlocked) {
|
||||
clearPQIdentityCache()
|
||||
setTick((t) => t + 1)
|
||||
}
|
||||
})
|
||||
return unsub
|
||||
}, [])
|
||||
|
||||
return useMemo(() => {
|
||||
if (!isUnlocked || !mnemonic) return null
|
||||
if (
|
||||
cache &&
|
||||
cache.mnemonic === mnemonic &&
|
||||
cache.chainID === opts.chainID &&
|
||||
cache.accountIdx === accountIdx &&
|
||||
cache.scheme === scheme
|
||||
) {
|
||||
return cache.account
|
||||
}
|
||||
const account = derivePQIdentity({
|
||||
mnemonic,
|
||||
chainID: opts.chainID,
|
||||
accountIdx,
|
||||
scheme,
|
||||
})
|
||||
cache = { mnemonic, chainID: opts.chainID, accountIdx, scheme, account }
|
||||
return account
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isUnlocked, mnemonic, opts.chainID, accountIdx, scheme, tick])
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* apps/web PQ facade.
|
||||
*
|
||||
* Web-app entry point for the canonical PQ stack. The implementation
|
||||
* lives in `@luxfi/wallet`'s `features/wallet/pq` slice — this file
|
||||
* re-exports the public surface so screens import a single stable path:
|
||||
*
|
||||
* import { derivePQIdentity, signHybridSecp256k1MLDSA, ... } from "../lib/pq"
|
||||
*
|
||||
* Rationale: the wallet pkg owns the byte-pinned domain separators,
|
||||
* the cSHAKE AccountID derivation, and the @noble/post-quantum
|
||||
* provider wrappers; apps/web is just the consumer. Keeping the
|
||||
* facade thin means there is exactly one place where the wallet's PQ
|
||||
* vocabulary is defined, and exactly one place to update on a
|
||||
* provider swap or LP-4200 precompile-input change.
|
||||
*
|
||||
* Canonical PQ stack (per LP-4200):
|
||||
*
|
||||
* ML-DSA-65 — FIPS 204 L3 — PQ identity, signing (0x012202)
|
||||
* ML-KEM-768 — FIPS 203 L3 — PQ-TLS, encrypted-DM (0x012201)
|
||||
* SLH-DSA-192f — FIPS 205 L3 — recovery / cold storage (0x012203)
|
||||
* Hybrid containers — Ed25519∥ML-DSA-65, secp256k1∥ML-DSA-65
|
||||
*
|
||||
* HD paths:
|
||||
*
|
||||
* m/44'/9000'/<chainID>'/0'/<roleIdx>'/<accountIdx>' — ML-DSA identity/tx/session
|
||||
* m/44'/9000'/<chainID>'/2'/<accountIdx>' — SLH-DSA recovery
|
||||
* m/44'/60'/0'/0/<accountIdx> — EVM C-Chain secp256k1
|
||||
*/
|
||||
|
||||
export type {
|
||||
PQAccount,
|
||||
AccountRole,
|
||||
MLDSAProvider,
|
||||
TxAuthEnvelope,
|
||||
SigningContext,
|
||||
KEMProvider,
|
||||
SLHDSAProvider,
|
||||
ClassicalScheme,
|
||||
HybridSignature,
|
||||
EthCallClient,
|
||||
} from "@luxfi/wallet/src/features/wallet/pq"
|
||||
|
||||
export {
|
||||
// pqAccount
|
||||
ACCOUNT_ID_SIZE,
|
||||
ACCOUNT_ROLE_IDENTITY,
|
||||
ACCOUNT_ROLE_RECOVERY,
|
||||
ACCOUNT_ROLE_SESSION,
|
||||
ACCOUNT_ROLE_TX,
|
||||
CSHAKE_CUSTOMIZATION_ACCOUNT_ID,
|
||||
CSHAKE_CUSTOMIZATION_IDENTITY,
|
||||
CSHAKE_CUSTOMIZATION_RECOVERY,
|
||||
CSHAKE_CUSTOMIZATION_SESSION,
|
||||
CSHAKE_CUSTOMIZATION_TX,
|
||||
WalletSchemeID,
|
||||
deriveAccountID,
|
||||
expandChildSeed,
|
||||
formatPQPath,
|
||||
formatRecoveryPath,
|
||||
isClassicalCompat,
|
||||
isPostQuantum,
|
||||
roleCustomization,
|
||||
roleHDIndex,
|
||||
signTx,
|
||||
walletSchemeName,
|
||||
|
||||
// domain
|
||||
PQ_CONTEXT_EVM_PRECOMPILE_MLDSA,
|
||||
PQ_CONTEXT_HANDSHAKE_MLKEM,
|
||||
PQ_CONTEXT_P_CHAIN_PLATFORM_MLDSA,
|
||||
PQ_CONTEXT_RECOVERY_SLHDSA,
|
||||
PQ_CONTEXT_X_CHAIN_UTXO_MLDSA,
|
||||
contextBytesFor,
|
||||
contextStringFor,
|
||||
|
||||
// mldsa
|
||||
mlDsa44Provider,
|
||||
mlDsa65Provider,
|
||||
mlDsa87Provider,
|
||||
providerFor,
|
||||
signWithContext,
|
||||
verifyWithContext,
|
||||
|
||||
// mlkem
|
||||
WALLET_KEM_SCHEME_MLKEM768,
|
||||
kemProviderByName,
|
||||
mlKem1024Provider,
|
||||
mlKem512Provider,
|
||||
mlKem768Provider,
|
||||
|
||||
// slhdsa
|
||||
slhDsa128fProvider,
|
||||
slhDsa192fProvider,
|
||||
slhDsa192sProvider,
|
||||
slhDsa256fProvider,
|
||||
slhDsaProviderByName,
|
||||
|
||||
// hybrid
|
||||
decodeHybrid,
|
||||
encodeHybrid,
|
||||
signHybridEd25519MLDSA,
|
||||
signHybridSecp256k1MLDSA,
|
||||
verifyHybridEd25519MLDSA,
|
||||
verifyHybridSecp256k1MLDSA,
|
||||
|
||||
// hdPq
|
||||
derivePQAccount,
|
||||
derivePQIdentity,
|
||||
deriveRecoveryAccount,
|
||||
|
||||
// precompile
|
||||
PRECOMPILE_MLDSA,
|
||||
PRECOMPILE_MLKEM,
|
||||
PRECOMPILE_SLHDSA,
|
||||
encodeMLDSAVerifyInput,
|
||||
encodeMLKEMVerifyInput,
|
||||
encodeSLHDSAVerifyInput,
|
||||
verifyMLDSAOnChain,
|
||||
} from "@luxfi/wallet/src/features/wallet/pq"
|
||||
@@ -13,7 +13,7 @@
|
||||
* (it returns the correct router per chain in its quote). The constants here
|
||||
* are only used by the on-chain QuoterV2 fallback when the gateway is offline.
|
||||
*
|
||||
* chain ids (per top-level memory):
|
||||
* Downstream EVM chain ids (per top-level memory):
|
||||
* 8675309 = mainnet, 8675310 = testnet, 8675311 = devnet.
|
||||
*
|
||||
* Lux EVM chain ids:
|
||||
@@ -114,7 +114,7 @@ const QUOTERS: Record<number, Address> = {
|
||||
96369: ZERO,
|
||||
// Zoo L1
|
||||
200200: ZERO,
|
||||
// mainnet/testnet/devnet
|
||||
// Downstream EVM mainnet/testnet/devnet
|
||||
8675309: ZERO,
|
||||
8675310: ZERO,
|
||||
8675311: ZERO,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Minimal Jest config for the pure-TS pkgs/wallet/src/features/wallet/pq
|
||||
// subtree. Bypasses the upstream-shaped jest-expo preset (which depends
|
||||
// on a missing config/jest-presets dir, see LLM.md) so the PQ tests can
|
||||
// run in isolation. No React Native deps — pure crypto over noble.
|
||||
|
||||
const path = require('path')
|
||||
|
||||
// pnpm hoists @noble/* to versioned virtual paths; the canonical
|
||||
// resolution points (node_modules/@noble/...) are the symlinks the
|
||||
// workspace consumes. For jest we map directly to the .pnpm store so
|
||||
// the resolver doesn't need to evaluate conditional exports.
|
||||
const workspace = path.resolve(__dirname, '../..')
|
||||
const pqStore = `${workspace}/node_modules/.pnpm/@noble+post-quantum@0.6.1/node_modules`
|
||||
|
||||
module.exports = {
|
||||
rootDir: 'src/features/wallet/pq',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['<rootDir>/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.[tj]sx?$': [
|
||||
'babel-jest',
|
||||
{
|
||||
presets: [
|
||||
['@babel/preset-env', { targets: { node: 'current' } }],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
// @noble/post-quantum is ESM-only; route its sub-entries through
|
||||
// physical .js paths in its pnpm-virtual node_modules.
|
||||
'^@noble/post-quantum/ml-dsa(\\.js)?$':
|
||||
`${pqStore}/@noble/post-quantum/ml-dsa.js`,
|
||||
'^@noble/post-quantum/ml-kem(\\.js)?$':
|
||||
`${pqStore}/@noble/post-quantum/ml-kem.js`,
|
||||
'^@noble/post-quantum/slh-dsa(\\.js)?$':
|
||||
`${pqStore}/@noble/post-quantum/slh-dsa.js`,
|
||||
'^@noble/post-quantum/utils(\\.js)?$':
|
||||
`${pqStore}/@noble/post-quantum/utils.js`,
|
||||
'^@noble/post-quantum/_crystals(\\.js)?$':
|
||||
`${pqStore}/@noble/post-quantum/_crystals.js`,
|
||||
// post-quantum internals reference `@noble/X/Y.js` with .js
|
||||
// extensions; route those through the pq pnpm-virtual store so
|
||||
// post-quantum gets its peer-pinned (curves 2.2.0, hashes 2.2.0,
|
||||
// ciphers 2.2.0). Imports WITHOUT .js (e.g. bip32's
|
||||
// `@noble/curves/secp256k1`) keep normal resolution and find their
|
||||
// own peer-pinned versions — bip32 1.3.2 needs curves 1.x.
|
||||
'^@noble/curves/(.+?)\\.js$':
|
||||
`${pqStore}/@noble/curves/$1.js`,
|
||||
'^@noble/hashes/(.+?)\\.js$':
|
||||
`${pqStore}/@noble/hashes/$1.js`,
|
||||
'^@noble/ciphers/(.+?)\\.js$':
|
||||
`${pqStore}/@noble/ciphers/$1.js`,
|
||||
},
|
||||
// @noble/post-quantum ships as native ESM; let babel-jest transform it.
|
||||
// pnpm stores packages under `node_modules/.pnpm/<scope>+<name>@<ver>/...`
|
||||
// The ignore pattern must let *all* @noble and @scure paths through —
|
||||
// whether they appear under the surface `node_modules/@noble/...` or
|
||||
// deeper at `node_modules/.pnpm/@noble+...`.
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(\\.pnpm/@(noble|scure)\\+|@noble|@scure))',
|
||||
],
|
||||
// ML-DSA-65 sign is ~50ms; SLH-DSA-192f sign is ~200ms in JS. Default 5s is tight.
|
||||
testTimeout: 120000,
|
||||
}
|
||||
+16
-13
@@ -26,22 +26,28 @@
|
||||
"@ethersproject/contracts": "5.7.0",
|
||||
"@ethersproject/providers": "5.7.2",
|
||||
"@gorhom/bottom-sheet": "5.2.8",
|
||||
"@react-navigation/core": "7.9.2",
|
||||
"@redux-saga/core": "1.2.3",
|
||||
"@reduxjs/toolkit": "1.9.3",
|
||||
"@scure/bip32": "1.3.2",
|
||||
"@shopify/react-native-skia": "2.2.20",
|
||||
"@tanstack/react-query": "5.90.20",
|
||||
"@l.x/api": "^1.0.8",
|
||||
"@l.x/gating": "^1.0.7",
|
||||
"@l.x/lx": "^1.0.8",
|
||||
"@l.x/sessions": "^1.0.8",
|
||||
"@l.x/ui": "^6.2.4",
|
||||
"@l.x/utils": "^1.1.3",
|
||||
"@luxamm/analytics-events": "2.43.2",
|
||||
"@luxamm/client-data-api": "0.0.164",
|
||||
"@luxamm/permit2-sdk": "1.3.3",
|
||||
"@luxamm/router-sdk": "2.7.4",
|
||||
"@luxamm/sdk-core": "7.12.3",
|
||||
"@luxamm/swap-sdk": "3.0.1",
|
||||
"@l.x/api": "^1.0.8",
|
||||
"@l.x/gating": "^1.0.7",
|
||||
"@l.x/sessions": "^1.0.8",
|
||||
"@noble/curves": "1.9.7",
|
||||
"@noble/hashes": "^1.7.1",
|
||||
"@noble/post-quantum": "0.6.1",
|
||||
"@react-navigation/core": "7.9.2",
|
||||
"@redux-saga/core": "1.2.3",
|
||||
"@reduxjs/toolkit": "1.9.3",
|
||||
"@scure/bip32": "1.3.2",
|
||||
"@scure/bip39": "1.6.0",
|
||||
"@shopify/react-native-skia": "2.2.20",
|
||||
"@tanstack/react-query": "5.90.20",
|
||||
"apollo3-cache-persist": "0.14.1",
|
||||
"dayjs": "1.11.7",
|
||||
"ethers": "5.7.2",
|
||||
@@ -70,20 +76,17 @@
|
||||
"redux-saga-test-plan": "4.0.4",
|
||||
"sp-react-native-in-app-updates": "1.5.0",
|
||||
"typed-redux-saga": "1.5.0",
|
||||
"@l.x/ui": "^6.2.4",
|
||||
"@l.x/lx": "^1.0.8",
|
||||
"@l.x/utils": "^1.1.3",
|
||||
"viem": "2.30.5",
|
||||
"zod": "npm:@hanzogui/zod@4.3.6-fork.1",
|
||||
"zxcvbn": "4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "7.6.0",
|
||||
"@luxfi/eslint-config": "^1.0.6",
|
||||
"@testing-library/react-native": "13.3.3",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/zxcvbn": "4.4.2",
|
||||
"@typescript/native-preview": "7.0.0-dev.20260311.1",
|
||||
"@luxfi/eslint-config": "^1.0.6",
|
||||
"depcheck": "1.4.7",
|
||||
"eslint": "8.57.1",
|
||||
"jest": "29.7.0",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
import {
|
||||
PQ_CONTEXT_EVM_PRECOMPILE_MLDSA,
|
||||
PQ_CONTEXT_HANDSHAKE_MLKEM,
|
||||
PQ_CONTEXT_P_CHAIN_PLATFORM_MLDSA,
|
||||
PQ_CONTEXT_RECOVERY_SLHDSA,
|
||||
PQ_CONTEXT_X_CHAIN_UTXO_MLDSA,
|
||||
contextBytesFor,
|
||||
contextStringFor,
|
||||
} from './domain'
|
||||
|
||||
describe('PQ domain separators', () => {
|
||||
it('pin byte content for the EVM precompile + X-Chain contexts', () => {
|
||||
// These strings are wire-compatibility critical. The Lux on-chain
|
||||
// verifier rebuilds the FIPS-204 M' prefix from the same literal,
|
||||
// so any drift here is a wallet-incompatible regression.
|
||||
expect(PQ_CONTEXT_EVM_PRECOMPILE_MLDSA).toBe('lux-evm-precompile-mldsa-v1')
|
||||
expect(PQ_CONTEXT_X_CHAIN_UTXO_MLDSA).toBe('lux-x-chain-utxo-v1')
|
||||
expect(PQ_CONTEXT_P_CHAIN_PLATFORM_MLDSA).toBe('lux-p-chain-platform-mldsa-v1')
|
||||
expect(PQ_CONTEXT_RECOVERY_SLHDSA).toBe('lux-recovery-slhdsa-v1')
|
||||
expect(PQ_CONTEXT_HANDSHAKE_MLKEM).toBe('lux-handshake-mlkem-v1')
|
||||
})
|
||||
|
||||
it('all contexts fit in the FIPS-204 §5.4 255-byte ctx limit', () => {
|
||||
for (const c of [
|
||||
'evm-precompile',
|
||||
'x-chain-utxo',
|
||||
'p-chain-platform',
|
||||
'recovery',
|
||||
'handshake',
|
||||
] as const) {
|
||||
const bytes = contextBytesFor(c)
|
||||
expect(bytes.length).toBeLessThanOrEqual(255)
|
||||
expect(bytes.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('contextStringFor returns the canonical string', () => {
|
||||
expect(contextStringFor('evm-precompile')).toBe(PQ_CONTEXT_EVM_PRECOMPILE_MLDSA)
|
||||
expect(contextStringFor('x-chain-utxo')).toBe(PQ_CONTEXT_X_CHAIN_UTXO_MLDSA)
|
||||
expect(contextStringFor('p-chain-platform')).toBe(PQ_CONTEXT_P_CHAIN_PLATFORM_MLDSA)
|
||||
expect(contextStringFor('recovery')).toBe(PQ_CONTEXT_RECOVERY_SLHDSA)
|
||||
expect(contextStringFor('handshake')).toBe(PQ_CONTEXT_HANDSHAKE_MLKEM)
|
||||
})
|
||||
|
||||
it('contextBytesFor is UTF-8 encoded', () => {
|
||||
const bytes = contextBytesFor('evm-precompile')
|
||||
expect(new TextDecoder().decode(bytes)).toBe(PQ_CONTEXT_EVM_PRECOMPILE_MLDSA)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
/**
|
||||
* Domain separators that bind an ML-DSA signature to a specific Lux
|
||||
* verifier context. These are *not* cSHAKE customizations (those bind
|
||||
* the AccountID derivation in pqAccount.ts); they are the FIPS-204 §5.4
|
||||
* `ctx` argument passed to ML-DSA Sign/Verify directly.
|
||||
*
|
||||
* Per FIPS 204 §5.4 the signer prefixes the message with
|
||||
*
|
||||
* M' = 0x00 || OctetString(|ctx|) || ctx || M
|
||||
*
|
||||
* before the rest of the signing transform. The same prefix is rebuilt
|
||||
* inside the verifier, so a signature produced under one context fails
|
||||
* verification under any other. This is the canonical replay protection
|
||||
* across chains and verifier surfaces.
|
||||
*
|
||||
* The strings here MUST match byte-for-byte the strings used by:
|
||||
* • the LP-4200 EVM precompile at 0x012202 (Lux C-Chain ML-DSA verifier)
|
||||
* • the X-Chain UTXO signing path in luxfi/node
|
||||
*
|
||||
* Changing any byte here is a wallet-incompatible regression. Pinned by
|
||||
* domain.test.ts.
|
||||
*/
|
||||
|
||||
/** ML-DSA context for the Lux EVM precompile at 0x012202 (LP-4200). */
|
||||
export const PQ_CONTEXT_EVM_PRECOMPILE_MLDSA: string = 'lux-evm-precompile-mldsa-v1'
|
||||
|
||||
/** ML-DSA context for X-Chain UTXO transactions. */
|
||||
export const PQ_CONTEXT_X_CHAIN_UTXO_MLDSA: string = 'lux-x-chain-utxo-v1'
|
||||
|
||||
/** ML-DSA context for P-Chain platform transactions. */
|
||||
export const PQ_CONTEXT_P_CHAIN_PLATFORM_MLDSA: string = 'lux-p-chain-platform-mldsa-v1'
|
||||
|
||||
/** SLH-DSA context for cold-storage / recovery signatures (LP-4200 0x012203). */
|
||||
export const PQ_CONTEXT_RECOVERY_SLHDSA: string = 'lux-recovery-slhdsa-v1'
|
||||
|
||||
/** ML-KEM context for the wallet's PQ-TLS / encrypted-DM hybrid handshake. */
|
||||
export const PQ_CONTEXT_HANDSHAKE_MLKEM: string = 'lux-handshake-mlkem-v1'
|
||||
|
||||
/**
|
||||
* SigningContext is the routing key the wallet uses to pick a domain
|
||||
* separator before signing. Naming the value (the *what*) rather than
|
||||
* the place (the *where*) so the same enum reads cleanly from any
|
||||
* call site: a screen, a saga, an injected provider, etc.
|
||||
*/
|
||||
export type SigningContext =
|
||||
| 'evm-precompile'
|
||||
| 'x-chain-utxo'
|
||||
| 'p-chain-platform'
|
||||
| 'recovery'
|
||||
| 'handshake'
|
||||
|
||||
/** Look up the canonical context bytes for a SigningContext value. */
|
||||
export function contextStringFor(ctx: SigningContext): string {
|
||||
switch (ctx) {
|
||||
case 'evm-precompile':
|
||||
return PQ_CONTEXT_EVM_PRECOMPILE_MLDSA
|
||||
case 'x-chain-utxo':
|
||||
return PQ_CONTEXT_X_CHAIN_UTXO_MLDSA
|
||||
case 'p-chain-platform':
|
||||
return PQ_CONTEXT_P_CHAIN_PLATFORM_MLDSA
|
||||
case 'recovery':
|
||||
return PQ_CONTEXT_RECOVERY_SLHDSA
|
||||
case 'handshake':
|
||||
return PQ_CONTEXT_HANDSHAKE_MLKEM
|
||||
default: {
|
||||
const _exhaustive: never = ctx
|
||||
throw new Error(`pq/domain: unknown signing context ${_exhaustive as string}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Encode a context string as UTF-8 bytes for the FIPS-204 ctx parameter. */
|
||||
export function contextBytesFor(ctx: SigningContext): Uint8Array {
|
||||
const s = contextStringFor(ctx)
|
||||
if (s.length > 255) {
|
||||
// FIPS 204 §5.4 limits |ctx| to 255 bytes. Our strings are well under
|
||||
// this; the guard is defensive against future edits.
|
||||
throw new Error(`pq/domain: context string for ${ctx} exceeds FIPS-204 255-byte limit`)
|
||||
}
|
||||
return new TextEncoder().encode(s)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
import {
|
||||
derivePQAccount,
|
||||
derivePQIdentity,
|
||||
deriveRecoveryAccount,
|
||||
} from './hdPq'
|
||||
import { ACCOUNT_ID_SIZE, WalletSchemeID, formatPQPath, formatRecoveryPath } from './pqAccount'
|
||||
import { mlDsa65Provider } from './mldsa'
|
||||
import { slhDsa192fProvider } from './slhdsa'
|
||||
|
||||
// BIP-39 test vector — same mnemonic the upstream BIP-39 test suite uses
|
||||
// for "trezor" entropy. Stable across @scure/bip39 releases.
|
||||
const TEST_MNEMONIC =
|
||||
'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'
|
||||
|
||||
describe('HD PQ derivation', () => {
|
||||
describe('derivePQIdentity (ML-DSA-65 over m/44\'/9000\'/<chainID>\'/0\'/0\'/0\')', () => {
|
||||
it('produces a deterministic identity account for a given mnemonic + chain', () => {
|
||||
const a = derivePQIdentity({ mnemonic: TEST_MNEMONIC, chainID: 96369 })
|
||||
const b = derivePQIdentity({ mnemonic: TEST_MNEMONIC, chainID: 96369 })
|
||||
expect(a.publicKey).toEqual(b.publicKey)
|
||||
expect(a.privateKey).toEqual(b.privateKey)
|
||||
expect(a.accountID).toEqual(b.accountID)
|
||||
})
|
||||
|
||||
it('uses the canonical Lux PQ HD path', () => {
|
||||
const acct = derivePQIdentity({ mnemonic: TEST_MNEMONIC, chainID: 96369 })
|
||||
// formatPQPath(chainID=96369, roleIdx=0 (identity), accountIdx=0)
|
||||
expect(acct.derivationPath).toBe(formatPQPath(96369, 0, 0))
|
||||
expect(acct.derivationPath).toBe("m/44'/9000'/96369'/0'/0'/0'")
|
||||
})
|
||||
|
||||
it('emits ML-DSA-65 key material with the FIPS-204 byte widths', () => {
|
||||
const acct = derivePQIdentity({ mnemonic: TEST_MNEMONIC, chainID: 96369 })
|
||||
expect(acct.schemeID).toBe(WalletSchemeID.MLDSA65)
|
||||
expect(acct.publicKey.length).toBe(mlDsa65Provider.publicKeySize)
|
||||
expect(acct.privateKey?.length).toBe(mlDsa65Provider.privateKeySize)
|
||||
expect(acct.accountID.length).toBe(ACCOUNT_ID_SIZE)
|
||||
expect(acct.role).toBe('identity')
|
||||
})
|
||||
|
||||
it('two chains produce different keys from the same mnemonic — chain binding', () => {
|
||||
const a = derivePQIdentity({ mnemonic: TEST_MNEMONIC, chainID: 1 })
|
||||
const b = derivePQIdentity({ mnemonic: TEST_MNEMONIC, chainID: 2 })
|
||||
expect(a.publicKey).not.toEqual(b.publicKey)
|
||||
expect(a.accountID).not.toEqual(b.accountID)
|
||||
})
|
||||
|
||||
it('two account indices produce different keys — multi-account support', () => {
|
||||
const a = derivePQIdentity({ mnemonic: TEST_MNEMONIC, chainID: 96369, accountIdx: 0 })
|
||||
const b = derivePQIdentity({ mnemonic: TEST_MNEMONIC, chainID: 96369, accountIdx: 1 })
|
||||
expect(a.publicKey).not.toEqual(b.publicKey)
|
||||
})
|
||||
|
||||
it('two roles produce different keys — role binding (identity vs tx vs session)', () => {
|
||||
const identity = derivePQAccount({
|
||||
mnemonic: TEST_MNEMONIC, chainID: 96369, role: 'identity', accountIdx: 0,
|
||||
scheme: WalletSchemeID.MLDSA65,
|
||||
})
|
||||
const tx = derivePQAccount({
|
||||
mnemonic: TEST_MNEMONIC, chainID: 96369, role: 'tx', accountIdx: 0,
|
||||
scheme: WalletSchemeID.MLDSA65,
|
||||
})
|
||||
const session = derivePQAccount({
|
||||
mnemonic: TEST_MNEMONIC, chainID: 96369, role: 'session', accountIdx: 0,
|
||||
scheme: WalletSchemeID.MLDSA65,
|
||||
})
|
||||
expect(identity.publicKey).not.toEqual(tx.publicKey)
|
||||
expect(tx.publicKey).not.toEqual(session.publicKey)
|
||||
expect(identity.publicKey).not.toEqual(session.publicKey)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deriveRecoveryAccount (SLH-DSA-192f over m/44\'/9000\'/<chainID>\'/2\'/0\')', () => {
|
||||
it('produces a deterministic recovery keypair', () => {
|
||||
const a = deriveRecoveryAccount({ mnemonic: TEST_MNEMONIC, chainID: 96369 })
|
||||
const b = deriveRecoveryAccount({ mnemonic: TEST_MNEMONIC, chainID: 96369 })
|
||||
expect(a.publicKey).toEqual(b.publicKey)
|
||||
expect(a.privateKey).toEqual(b.privateKey)
|
||||
}, 60000)
|
||||
|
||||
it('uses the canonical Lux recovery HD path', () => {
|
||||
const acct = deriveRecoveryAccount({ mnemonic: TEST_MNEMONIC, chainID: 96369 })
|
||||
expect(acct.derivationPath).toBe(formatRecoveryPath(96369, 0))
|
||||
expect(acct.derivationPath).toBe("m/44'/9000'/96369'/2'/0'")
|
||||
}, 60000)
|
||||
|
||||
it('emits SLH-DSA-192f key material with the FIPS-205 byte widths', () => {
|
||||
const acct = deriveRecoveryAccount({ mnemonic: TEST_MNEMONIC, chainID: 96369 })
|
||||
expect(acct.providerName).toBe('slh-dsa-sha2-192f')
|
||||
expect(acct.publicKey.length).toBe(slhDsa192fProvider.publicKeySize)
|
||||
expect(acct.privateKey.length).toBe(slhDsa192fProvider.privateKeySize)
|
||||
}, 60000)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,150 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
/**
|
||||
* HD derivation for PQ accounts.
|
||||
*
|
||||
* Composition pipeline (mirrors luxfi/sdk wallet/keychain pq derivation):
|
||||
*
|
||||
* BIP-39 mnemonic
|
||||
* → BIP-39 seed (PBKDF2-SHA512, "mnemonic"+passphrase, 2048 iters, 64 bytes)
|
||||
* → BIP-32 master key (HMAC-SHA512 over "Bitcoin seed")
|
||||
* → BIP-32 child node at `m/44'/9000'/<chainID>'/0'/<roleIdx>'/<accountIdx>'`
|
||||
* → 32-byte chainCode || privateKey (256 bits)
|
||||
* → expandChildSeed(privateKey, customization, 32)
|
||||
* → ξ — the 32-byte FIPS-204 seed
|
||||
* → MLDSAProvider.newKeyFromSeed(ξ)
|
||||
* → (publicKey, privateKey) for the chosen ML-DSA parameter set
|
||||
*
|
||||
* The cSHAKE expansion is the domain separator: with role-specific
|
||||
* customization strings ("LUX/WALLET/IDENTITY/V1", "LUX/WALLET/TX/V1",
|
||||
* "LUX/WALLET/SESSION/V1") two accounts sharing the same BIP-32 leaf
|
||||
* still produce uncorrelated ML-DSA keys, so a leaked identity key
|
||||
* cannot be used to derive the tx key.
|
||||
*
|
||||
* Recovery keys live on a parallel branch under coin-type 9000 with
|
||||
* change-index 2 and SLH-DSA seed expansion.
|
||||
*/
|
||||
|
||||
import { HDKey } from '@scure/bip32'
|
||||
import { mnemonicToSeedSync } from '@scure/bip39'
|
||||
import {
|
||||
type AccountRole,
|
||||
type MLDSAProvider,
|
||||
type PQAccount,
|
||||
WalletSchemeID,
|
||||
deriveAccountID,
|
||||
expandChildSeed,
|
||||
formatPQPath,
|
||||
formatRecoveryPath,
|
||||
roleCustomization,
|
||||
roleHDIndex,
|
||||
CSHAKE_CUSTOMIZATION_RECOVERY,
|
||||
} from './pqAccount'
|
||||
import { providerFor } from './mldsa'
|
||||
import { type SLHDSAProvider, slhDsa192fProvider } from './slhdsa'
|
||||
|
||||
/** Derive the canonical PQ identity account for an unlocked mnemonic. */
|
||||
export function derivePQIdentity(opts: {
|
||||
mnemonic: string
|
||||
chainID: number
|
||||
accountIdx?: number
|
||||
scheme?: WalletSchemeID
|
||||
passphrase?: string
|
||||
}): PQAccount {
|
||||
return derivePQAccount({
|
||||
mnemonic: opts.mnemonic,
|
||||
chainID: opts.chainID,
|
||||
role: 'identity',
|
||||
accountIdx: opts.accountIdx ?? 0,
|
||||
scheme: opts.scheme ?? WalletSchemeID.MLDSA65,
|
||||
passphrase: opts.passphrase,
|
||||
})
|
||||
}
|
||||
|
||||
/** Derive a per-role ML-DSA account. role='identity' is the canonical PQ identity. */
|
||||
export function derivePQAccount(opts: {
|
||||
mnemonic: string
|
||||
chainID: number
|
||||
role: Exclude<AccountRole, 'recovery'>
|
||||
accountIdx: number
|
||||
scheme: WalletSchemeID
|
||||
passphrase?: string
|
||||
}): PQAccount {
|
||||
const provider = providerFor(opts.scheme)
|
||||
const roleIdx = roleHDIndex(opts.role)
|
||||
const customization = roleCustomization(opts.role)
|
||||
const path = formatPQPath(opts.chainID, roleIdx, opts.accountIdx)
|
||||
|
||||
const childPriv = deriveBip32ChildPrivkey(opts.mnemonic, path, opts.passphrase)
|
||||
const seed = expandChildSeed(childPriv, customization, 32)
|
||||
const { publicKey, privateKey } = provider.newKeyFromSeed(seed)
|
||||
const accountID = deriveAccountID(opts.chainID, opts.scheme, publicKey)
|
||||
|
||||
return {
|
||||
accountID,
|
||||
schemeID: opts.scheme,
|
||||
publicKey,
|
||||
privateKey,
|
||||
role: opts.role,
|
||||
derivationPath: path,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive an SLH-DSA recovery account on the parallel branch. Returns
|
||||
* the canonical 192f recovery account unless a different provider is
|
||||
* supplied (e.g. 128f for lightweight cold storage).
|
||||
*/
|
||||
export function deriveRecoveryAccount(opts: {
|
||||
mnemonic: string
|
||||
chainID: number
|
||||
accountIdx?: number
|
||||
passphrase?: string
|
||||
provider?: SLHDSAProvider
|
||||
}): {
|
||||
publicKey: Uint8Array
|
||||
privateKey: Uint8Array
|
||||
derivationPath: string
|
||||
providerName: string
|
||||
} {
|
||||
const provider = opts.provider ?? slhDsa192fProvider
|
||||
const accountIdx = opts.accountIdx ?? 0
|
||||
const path = formatRecoveryPath(opts.chainID, accountIdx)
|
||||
const childPriv = deriveBip32ChildPrivkey(opts.mnemonic, path, opts.passphrase)
|
||||
// SLH-DSA expects a seed roughly the size of the secret-key blocks.
|
||||
// We expand the 32-byte BIP-32 leaf to the provider's seed/secret-key
|
||||
// width via the canonical Lux cSHAKE recovery customization. For
|
||||
// SLH-DSA-192f noble takes its 96-byte secret key internally from
|
||||
// keygen(seed) when seed is the right length; noble's slh_dsa keygen
|
||||
// accepts seed length 3*N (where N is the security parameter in bytes
|
||||
// — 24 for 192). We expand to 3*24 = 72 bytes.
|
||||
const seedLen = provider.privateKeySize === 96 ? 72 : provider.privateKeySize
|
||||
const seed = expandChildSeed(childPriv, CSHAKE_CUSTOMIZATION_RECOVERY, seedLen)
|
||||
const { publicKey, privateKey } = provider.keygen(seed)
|
||||
return {
|
||||
publicKey,
|
||||
privateKey,
|
||||
derivationPath: path,
|
||||
providerName: provider.name,
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert a BIP-39 mnemonic into a 64-byte seed and run a BIP-32 derivation. */
|
||||
function deriveBip32ChildPrivkey(
|
||||
mnemonic: string,
|
||||
path: string,
|
||||
passphrase?: string,
|
||||
): Uint8Array {
|
||||
const seed = mnemonicToSeedSync(mnemonic.normalize('NFKD').trim(), passphrase ?? '')
|
||||
const root = HDKey.fromMasterSeed(seed)
|
||||
const child = root.derive(path)
|
||||
if (!child.privateKey) {
|
||||
throw new Error(`pq/hdPq: derivation returned no private key for ${path}`)
|
||||
}
|
||||
return child.privateKey
|
||||
}
|
||||
|
||||
/** Re-export so consumers don't need a second import. */
|
||||
export { providerFor } from './mldsa'
|
||||
export type { MLDSAProvider }
|
||||
@@ -0,0 +1,170 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
import { secp256k1 } from '@noble/curves/secp256k1'
|
||||
import { ed25519 } from '@noble/curves/ed25519'
|
||||
import {
|
||||
decodeHybrid,
|
||||
encodeHybrid,
|
||||
signHybridEd25519MLDSA,
|
||||
signHybridSecp256k1MLDSA,
|
||||
verifyHybridEd25519MLDSA,
|
||||
verifyHybridSecp256k1MLDSA,
|
||||
} from './hybrid'
|
||||
import { mlDsa65Provider } from './mldsa'
|
||||
import { WalletSchemeID } from './pqAccount'
|
||||
|
||||
describe('hybrid signature container', () => {
|
||||
it('encode → decode round-trip preserves both halves byte-exactly', () => {
|
||||
const classical = new Uint8Array(65).fill(0xab)
|
||||
const pq = new Uint8Array(3309).fill(0xcd)
|
||||
const wire = encodeHybrid(classical, pq)
|
||||
expect(wire.length).toBe(2 + 65 + 2 + 3309)
|
||||
const got = decodeHybrid(wire)
|
||||
expect(got.classical).toEqual(classical)
|
||||
expect(got.pq).toEqual(pq)
|
||||
})
|
||||
|
||||
it('encodes length prefixes in big-endian', () => {
|
||||
const classical = new Uint8Array(65)
|
||||
const pq = new Uint8Array(3309)
|
||||
const wire = encodeHybrid(classical, pq)
|
||||
// u16be(65) = 0x00 0x41
|
||||
// u16be(3309) = 0x0C 0xED
|
||||
expect(wire[0]).toBe(0x00)
|
||||
expect(wire[1]).toBe(0x41)
|
||||
expect(wire[2 + 65]).toBe(0x0c)
|
||||
expect(wire[2 + 65 + 1]).toBe(0xed)
|
||||
})
|
||||
|
||||
it('decodeHybrid throws on truncation', () => {
|
||||
expect(() => decodeHybrid(new Uint8Array(3))).toThrow(/too short/)
|
||||
// Length prefix promises 100 classical bytes but buffer is too small.
|
||||
const truncated = new Uint8Array(2 + 50 + 2)
|
||||
truncated[1] = 100
|
||||
expect(() => decodeHybrid(truncated)).toThrow(/truncated/)
|
||||
})
|
||||
|
||||
it('decodeHybrid throws on length mismatch', () => {
|
||||
const oversized = encodeHybrid(new Uint8Array(10), new Uint8Array(20))
|
||||
// Append a stray byte → total no longer matches the prefixed sum.
|
||||
const tampered = new Uint8Array(oversized.length + 1)
|
||||
tampered.set(oversized)
|
||||
expect(() => decodeHybrid(tampered)).toThrow(/does not match/)
|
||||
})
|
||||
|
||||
describe('secp256k1 + ML-DSA-65 hybrid (EVM-shaped)', () => {
|
||||
it('sign → verify round-trip succeeds', () => {
|
||||
// Use a deterministic ML-DSA seed; secp256k1 sk can be any 32 bytes
|
||||
// in [1, n-1] — we pick a clean value below the curve order.
|
||||
const classicalSk = new Uint8Array(32)
|
||||
classicalSk[31] = 0x42
|
||||
const classicalPk = secp256k1.getPublicKey(classicalSk, false)
|
||||
|
||||
const pqSeed = new Uint8Array(32).fill(7)
|
||||
const { publicKey: pqPk, privateKey: pqSk } = mlDsa65Provider.newKeyFromSeed(pqSeed)
|
||||
|
||||
const msg = new TextEncoder().encode('hybrid-evm-tx-blob')
|
||||
const sig = signHybridSecp256k1MLDSA({
|
||||
classicalSk,
|
||||
pqSk,
|
||||
pqScheme: WalletSchemeID.MLDSA65,
|
||||
msg,
|
||||
ctx: 'evm-precompile',
|
||||
})
|
||||
|
||||
const ok = verifyHybridSecp256k1MLDSA({
|
||||
classicalPk,
|
||||
pqPk,
|
||||
pqScheme: WalletSchemeID.MLDSA65,
|
||||
msg,
|
||||
signature: sig,
|
||||
ctx: 'evm-precompile',
|
||||
})
|
||||
expect(ok).toBe(true)
|
||||
})
|
||||
|
||||
it('verification fails when classical pk is wrong', () => {
|
||||
const classicalSk = new Uint8Array(32); classicalSk[31] = 0x11
|
||||
const wrongPk = secp256k1.getPublicKey(new Uint8Array(32).fill(0xaa), false)
|
||||
|
||||
const pqSeed = new Uint8Array(32).fill(8)
|
||||
const { publicKey: pqPk, privateKey: pqSk } = mlDsa65Provider.newKeyFromSeed(pqSeed)
|
||||
const msg = new TextEncoder().encode('hybrid-evm-tx')
|
||||
const sig = signHybridSecp256k1MLDSA({
|
||||
classicalSk, pqSk, pqScheme: WalletSchemeID.MLDSA65, msg, ctx: 'evm-precompile',
|
||||
})
|
||||
expect(
|
||||
verifyHybridSecp256k1MLDSA({
|
||||
classicalPk: wrongPk,
|
||||
pqPk,
|
||||
pqScheme: WalletSchemeID.MLDSA65,
|
||||
msg,
|
||||
signature: sig,
|
||||
ctx: 'evm-precompile',
|
||||
}),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('verification fails when pq pk is wrong (catches a forged classical-only sig)', () => {
|
||||
const classicalSk = new Uint8Array(32); classicalSk[31] = 0x22
|
||||
const classicalPk = secp256k1.getPublicKey(classicalSk, false)
|
||||
const pqSeed = new Uint8Array(32).fill(9)
|
||||
const { privateKey: pqSk } = mlDsa65Provider.newKeyFromSeed(pqSeed)
|
||||
const wrongPq = mlDsa65Provider.newKeyFromSeed(new Uint8Array(32).fill(99))
|
||||
const msg = new TextEncoder().encode('hybrid-evm-tx-2')
|
||||
const sig = signHybridSecp256k1MLDSA({
|
||||
classicalSk, pqSk, pqScheme: WalletSchemeID.MLDSA65, msg, ctx: 'evm-precompile',
|
||||
})
|
||||
expect(
|
||||
verifyHybridSecp256k1MLDSA({
|
||||
classicalPk,
|
||||
pqPk: wrongPq.publicKey,
|
||||
pqScheme: WalletSchemeID.MLDSA65,
|
||||
msg,
|
||||
signature: sig,
|
||||
ctx: 'evm-precompile',
|
||||
}),
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Ed25519 + ML-DSA-65 hybrid (X-Chain-shaped)', () => {
|
||||
it('sign → verify round-trip succeeds', () => {
|
||||
const classicalSk = new Uint8Array(32).fill(3)
|
||||
const classicalPk = ed25519.getPublicKey(classicalSk)
|
||||
|
||||
const pqSeed = new Uint8Array(32).fill(17)
|
||||
const { publicKey: pqPk, privateKey: pqSk } = mlDsa65Provider.newKeyFromSeed(pqSeed)
|
||||
|
||||
const msg = new TextEncoder().encode('hybrid-x-chain-utxo')
|
||||
const sig = signHybridEd25519MLDSA({
|
||||
classicalSk, pqSk, pqScheme: WalletSchemeID.MLDSA65, msg, ctx: 'x-chain-utxo',
|
||||
})
|
||||
|
||||
const ok = verifyHybridEd25519MLDSA({
|
||||
classicalPk, pqPk, pqScheme: WalletSchemeID.MLDSA65, msg, signature: sig, ctx: 'x-chain-utxo',
|
||||
})
|
||||
expect(ok).toBe(true)
|
||||
})
|
||||
|
||||
it('verification fails under the wrong ctx (cross-chain replay)', () => {
|
||||
const classicalSk = new Uint8Array(32).fill(4)
|
||||
const classicalPk = ed25519.getPublicKey(classicalSk)
|
||||
const pqSeed = new Uint8Array(32).fill(19)
|
||||
const { publicKey: pqPk, privateKey: pqSk } = mlDsa65Provider.newKeyFromSeed(pqSeed)
|
||||
const msg = new TextEncoder().encode('cross-ctx-replay-bait')
|
||||
// Mint under X-Chain ctx, try to verify as EVM precompile.
|
||||
const sig = signHybridEd25519MLDSA({
|
||||
classicalSk, pqSk, pqScheme: WalletSchemeID.MLDSA65, msg, ctx: 'x-chain-utxo',
|
||||
})
|
||||
// classical half verifies fine (no ctx), but PQ half must fail
|
||||
// because ctx is different → verify returns false.
|
||||
expect(
|
||||
verifyHybridEd25519MLDSA({
|
||||
classicalPk, pqPk, pqScheme: WalletSchemeID.MLDSA65, msg, signature: sig, ctx: 'evm-precompile',
|
||||
}),
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,262 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
/**
|
||||
* Hybrid classical + post-quantum signature container.
|
||||
*
|
||||
* During the PQ transition era, every wallet signature is produced by
|
||||
* BOTH a classical scheme (secp256k1 for EVM, Ed25519 for X-Chain
|
||||
* native) AND ML-DSA-65. Either signature alone is acceptable to a
|
||||
* verifier that runs in classical-or-PQ mode; a verifier in strict-PQ
|
||||
* mode requires the ML-DSA half. This is the canonical hedge against
|
||||
* a discovery-day PQ attack (CRQC) without giving up classical
|
||||
* verification on chains that have not yet enabled the precompile.
|
||||
*
|
||||
* Wire format (byte-identical to luxfi/sdk wallet/keychain/PQSigner.Sign
|
||||
* for KeyTypeHybridSecp256k1MLDSA*):
|
||||
*
|
||||
* u16be(|classicalSig|) || classicalSig || u16be(|pqSig|) || pqSig
|
||||
*
|
||||
* This is the same encoding the Go side emits, so a tx signed in the
|
||||
* browser is wire-compatible with the Go relayer / RPC submission path.
|
||||
*
|
||||
* Domain separation: classical and PQ halves sign DIFFERENT byte
|
||||
* strings — the classical half signs the keccak256 (EVM) or sha256
|
||||
* (UTXO) digest of the unsigned tx, while the PQ half signs the raw
|
||||
* tx under a FIPS-204 ctx. This matches the precompile-side verifier
|
||||
* pair: 0xEC verifies classical, 0x012202 verifies PQ. A weakened
|
||||
* classical scheme cannot forge a PQ signature, and vice versa, because
|
||||
* the two halves don't share a digest function.
|
||||
*/
|
||||
|
||||
import { secp256k1 } from '@noble/curves/secp256k1'
|
||||
import { ed25519 } from '@noble/curves/ed25519'
|
||||
import { keccak_256 } from '@noble/hashes/sha3'
|
||||
import { sha256 } from '@noble/hashes/sha256'
|
||||
import { type MLDSAProvider, WalletSchemeID } from './pqAccount'
|
||||
import { providerFor, signWithContext, verifyWithContext } from './mldsa'
|
||||
import { type SigningContext } from './domain'
|
||||
|
||||
/** Classical scheme variant inside a hybrid container. */
|
||||
export type ClassicalScheme = 'secp256k1' | 'ed25519'
|
||||
|
||||
/**
|
||||
* HybridSignature is the in-memory shape of a parsed hybrid container.
|
||||
*
|
||||
* The wire-encoded form is the length-prefixed concatenation
|
||||
* (encodeHybrid). Consumers should round-trip through encode/decode
|
||||
* rather than mutating the in-memory shape directly.
|
||||
*/
|
||||
export interface HybridSignature {
|
||||
classical: Uint8Array
|
||||
pq: Uint8Array
|
||||
classicalScheme: ClassicalScheme
|
||||
pqScheme: WalletSchemeID
|
||||
}
|
||||
|
||||
/**
|
||||
* encodeHybrid produces the wire bytes for a hybrid signature.
|
||||
*
|
||||
* u16be(|classical|) || classical || u16be(|pq|) || pq
|
||||
*
|
||||
* The format is intentionally length-prefixed (not delimiter-based) so
|
||||
* a verifier can split the two halves without scheme knowledge — the
|
||||
* scheme bytes live one layer up in the auth envelope.
|
||||
*/
|
||||
export function encodeHybrid(classical: Uint8Array, pq: Uint8Array): Uint8Array {
|
||||
if (classical.length > 0xffff) {
|
||||
throw new Error(`pq/hybrid: classical signature ${classical.length} bytes exceeds u16 limit`)
|
||||
}
|
||||
if (pq.length > 0xffff) {
|
||||
throw new Error(`pq/hybrid: pq signature ${pq.length} bytes exceeds u16 limit`)
|
||||
}
|
||||
const out = new Uint8Array(2 + classical.length + 2 + pq.length)
|
||||
let o = 0
|
||||
out[o++] = (classical.length >> 8) & 0xff
|
||||
out[o++] = classical.length & 0xff
|
||||
out.set(classical, o)
|
||||
o += classical.length
|
||||
out[o++] = (pq.length >> 8) & 0xff
|
||||
out[o++] = pq.length & 0xff
|
||||
out.set(pq, o)
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* decodeHybrid splits a wire-encoded hybrid signature back into its
|
||||
* (classical, pq) halves. Throws on a truncated or malformed buffer.
|
||||
*/
|
||||
export function decodeHybrid(wire: Uint8Array): { classical: Uint8Array; pq: Uint8Array } {
|
||||
if (wire.length < 4) {
|
||||
throw new Error(`pq/hybrid: wire ${wire.length} bytes too short for two length prefixes`)
|
||||
}
|
||||
const cLen = (wire[0] << 8) | wire[1]
|
||||
if (wire.length < 2 + cLen + 2) {
|
||||
throw new Error(`pq/hybrid: wire truncated after classical signature (${cLen} bytes expected)`)
|
||||
}
|
||||
const classical = wire.slice(2, 2 + cLen)
|
||||
const pqLen = (wire[2 + cLen] << 8) | wire[2 + cLen + 1]
|
||||
const want = 2 + cLen + 2 + pqLen
|
||||
if (wire.length !== want) {
|
||||
throw new Error(`pq/hybrid: wire length ${wire.length} does not match prefixed total ${want}`)
|
||||
}
|
||||
const pq = wire.slice(2 + cLen + 2, want)
|
||||
return { classical, pq }
|
||||
}
|
||||
|
||||
/**
|
||||
* signHybridSecp256k1MLDSA signs a transaction in EVM-shaped hybrid
|
||||
* mode:
|
||||
*
|
||||
* classical = secp256k1.sign(keccak256(msg), classicalSk)
|
||||
* pq = ML-DSA-65.sign(msg, pqSk, ctx)
|
||||
*
|
||||
* The classical half follows EVM convention (sign the keccak256 digest);
|
||||
* the PQ half signs the raw tx bytes under the supplied ctx. Returns
|
||||
* the encoded hybrid container.
|
||||
*/
|
||||
export function signHybridSecp256k1MLDSA(opts: {
|
||||
classicalSk: Uint8Array
|
||||
pqSk: Uint8Array
|
||||
pqScheme: WalletSchemeID
|
||||
msg: Uint8Array
|
||||
ctx: SigningContext
|
||||
/** Optional extra entropy for ML-DSA randomized signing. */
|
||||
extraEntropy?: Uint8Array
|
||||
}): Uint8Array {
|
||||
const digest = keccak_256(opts.msg)
|
||||
const classicalSig = secp256k1.sign(digest, opts.classicalSk)
|
||||
// serialize secp256k1 signature as r||s||v (65 bytes) to match EVM convention
|
||||
const classicalBytes = serializeSecpSignature(classicalSig, digest, opts.classicalSk)
|
||||
|
||||
const provider = providerFor(opts.pqScheme)
|
||||
const pqBytes = signWithContext(provider, opts.pqSk, opts.msg, opts.ctx)
|
||||
return encodeHybrid(classicalBytes, pqBytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* signHybridEd25519MLDSA signs a transaction in X-Chain-shaped hybrid
|
||||
* mode:
|
||||
*
|
||||
* classical = ed25519.sign(sha256(msg), classicalSk)
|
||||
* pq = ML-DSA-65.sign(msg, pqSk, ctx)
|
||||
*
|
||||
* Ed25519 over sha256(msg) matches the X-Chain UTXO transcript layout.
|
||||
*/
|
||||
export function signHybridEd25519MLDSA(opts: {
|
||||
classicalSk: Uint8Array
|
||||
pqSk: Uint8Array
|
||||
pqScheme: WalletSchemeID
|
||||
msg: Uint8Array
|
||||
ctx: SigningContext
|
||||
}): Uint8Array {
|
||||
const digest = sha256(opts.msg)
|
||||
const classicalSig = ed25519.sign(digest, opts.classicalSk)
|
||||
const provider = providerFor(opts.pqScheme)
|
||||
const pqSig = signWithContext(provider, opts.pqSk, opts.msg, opts.ctx)
|
||||
return encodeHybrid(classicalSig, pqSig)
|
||||
}
|
||||
|
||||
/**
|
||||
* verifyHybridSecp256k1MLDSA checks both halves of an EVM-shaped hybrid
|
||||
* signature. Returns true ONLY when both checks pass.
|
||||
*/
|
||||
export function verifyHybridSecp256k1MLDSA(opts: {
|
||||
classicalPk: Uint8Array
|
||||
pqPk: Uint8Array
|
||||
pqScheme: WalletSchemeID
|
||||
msg: Uint8Array
|
||||
signature: Uint8Array
|
||||
ctx: SigningContext
|
||||
}): boolean {
|
||||
const { classical, pq } = decodeHybrid(opts.signature)
|
||||
const digest = keccak_256(opts.msg)
|
||||
// secp256k1 signature wire form is r(32)||s(32)||v(1) — strip v before
|
||||
// verifying with @noble/curves which doesn't consume the recovery byte.
|
||||
const sigCore = classical.length === 65 ? classical.slice(0, 64) : classical
|
||||
const classicalOk = secp256k1.verify(sigCore, digest, opts.classicalPk)
|
||||
if (!classicalOk) return false
|
||||
const provider = providerFor(opts.pqScheme)
|
||||
return verifyWithContext(provider, opts.pqPk, opts.msg, pq, opts.ctx)
|
||||
}
|
||||
|
||||
/**
|
||||
* verifyHybridEd25519MLDSA checks both halves of an X-Chain-shaped
|
||||
* hybrid signature.
|
||||
*/
|
||||
export function verifyHybridEd25519MLDSA(opts: {
|
||||
classicalPk: Uint8Array
|
||||
pqPk: Uint8Array
|
||||
pqScheme: WalletSchemeID
|
||||
msg: Uint8Array
|
||||
signature: Uint8Array
|
||||
ctx: SigningContext
|
||||
}): boolean {
|
||||
const { classical, pq } = decodeHybrid(opts.signature)
|
||||
const digest = sha256(opts.msg)
|
||||
const classicalOk = ed25519.verify(classical, digest, opts.classicalPk)
|
||||
if (!classicalOk) return false
|
||||
const provider: MLDSAProvider = providerFor(opts.pqScheme)
|
||||
return verifyWithContext(provider, opts.pqPk, opts.msg, pq, opts.ctx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper: serialize a noble-secp256k1 signature plus recovery byte into
|
||||
* the EVM 65-byte r||s||v wire form. v ∈ {27,28} matches the Ethereum
|
||||
* convention used by 0xEC (ecrecover).
|
||||
*/
|
||||
function serializeSecpSignature(
|
||||
sig: ReturnType<typeof secp256k1.sign>,
|
||||
digest: Uint8Array,
|
||||
sk: Uint8Array,
|
||||
): Uint8Array {
|
||||
// noble's Signature object exposes r,s as bigints + recovery on `.recovery`.
|
||||
const r = bigintTo32(sig.r)
|
||||
const s = bigintTo32(sig.s)
|
||||
const rec = sig.recovery
|
||||
if (rec !== 0 && rec !== 1) {
|
||||
// Fall back to ecrecover-derived recovery if the noble Signature
|
||||
// didn't capture one; recompute by trying both.
|
||||
const v0 = secp256k1.Signature.fromCompact(new Uint8Array([...r, ...s])).addRecoveryBit(0)
|
||||
const v1 = secp256k1.Signature.fromCompact(new Uint8Array([...r, ...s])).addRecoveryBit(1)
|
||||
const pk = secp256k1.getPublicKey(sk, false)
|
||||
const tryRec = (v: typeof v0) => {
|
||||
try {
|
||||
return v.recoverPublicKey(digest).toRawBytes(false)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
const r0 = tryRec(v0)
|
||||
const r1 = tryRec(v1)
|
||||
const out = new Uint8Array(65)
|
||||
out.set(r, 0)
|
||||
out.set(s, 32)
|
||||
if (r0 && bytesEqual(r0, pk)) out[64] = 27
|
||||
else if (r1 && bytesEqual(r1, pk)) out[64] = 28
|
||||
else throw new Error('pq/hybrid: unable to recover secp256k1 v byte')
|
||||
return out
|
||||
}
|
||||
const out = new Uint8Array(65)
|
||||
out.set(r, 0)
|
||||
out.set(s, 32)
|
||||
out[64] = rec === 0 ? 27 : 28
|
||||
return out
|
||||
}
|
||||
|
||||
function bigintTo32(x: bigint): Uint8Array {
|
||||
const out = new Uint8Array(32)
|
||||
let v = x
|
||||
for (let i = 31; i >= 0; i--) {
|
||||
out[i] = Number(v & 0xffn)
|
||||
v >>= 8n
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
||||
if (a.length !== b.length) return false
|
||||
let diff = 0
|
||||
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]
|
||||
return diff === 0
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// pqAccount — shape, cSHAKE AccountID, HD-path strings, MLDSAProvider interface.
|
||||
export type { PQAccount, AccountRole, MLDSAProvider, TxAuthEnvelope } from './pqAccount'
|
||||
export {
|
||||
ACCOUNT_ID_SIZE,
|
||||
@@ -25,3 +26,75 @@ export {
|
||||
signTx,
|
||||
walletSchemeName,
|
||||
} from './pqAccount'
|
||||
|
||||
// domain — FIPS-204 ctx strings binding signatures to verifier surfaces.
|
||||
export type { SigningContext } from './domain'
|
||||
export {
|
||||
PQ_CONTEXT_EVM_PRECOMPILE_MLDSA,
|
||||
PQ_CONTEXT_HANDSHAKE_MLKEM,
|
||||
PQ_CONTEXT_P_CHAIN_PLATFORM_MLDSA,
|
||||
PQ_CONTEXT_RECOVERY_SLHDSA,
|
||||
PQ_CONTEXT_X_CHAIN_UTXO_MLDSA,
|
||||
contextBytesFor,
|
||||
contextStringFor,
|
||||
} from './domain'
|
||||
|
||||
// mldsa — concrete ML-DSA-44/65/87 providers backed by @noble/post-quantum.
|
||||
export {
|
||||
mlDsa44Provider,
|
||||
mlDsa65Provider,
|
||||
mlDsa87Provider,
|
||||
providerFor,
|
||||
signWithContext,
|
||||
verifyWithContext,
|
||||
} from './mldsa'
|
||||
|
||||
// mlkem — ML-KEM-512/768/1024 providers (KEMs).
|
||||
export type { KEMProvider } from './mlkem'
|
||||
export {
|
||||
WALLET_KEM_SCHEME_MLKEM768,
|
||||
kemProviderByName,
|
||||
mlKem1024Provider,
|
||||
mlKem512Provider,
|
||||
mlKem768Provider,
|
||||
} from './mlkem'
|
||||
|
||||
// slhdsa — SLH-DSA hash-based providers for recovery.
|
||||
export type { SLHDSAProvider } from './slhdsa'
|
||||
export {
|
||||
slhDsa128fProvider,
|
||||
slhDsa192fProvider,
|
||||
slhDsa192sProvider,
|
||||
slhDsa256fProvider,
|
||||
slhDsaProviderByName,
|
||||
} from './slhdsa'
|
||||
|
||||
// hybrid — classical ∥ PQ container, byte-compatible with luxfi/sdk PQSigner.
|
||||
export type { ClassicalScheme, HybridSignature } from './hybrid'
|
||||
export {
|
||||
decodeHybrid,
|
||||
encodeHybrid,
|
||||
signHybridEd25519MLDSA,
|
||||
signHybridSecp256k1MLDSA,
|
||||
verifyHybridEd25519MLDSA,
|
||||
verifyHybridSecp256k1MLDSA,
|
||||
} from './hybrid'
|
||||
|
||||
// hdPq — BIP-32 → expandChildSeed → ML-DSA / SLH-DSA keypair derivation.
|
||||
export {
|
||||
derivePQAccount,
|
||||
derivePQIdentity,
|
||||
deriveRecoveryAccount,
|
||||
} from './hdPq'
|
||||
|
||||
// precompile — eth_call wrappers for the LP-4200 0x0122xx verifiers.
|
||||
export type { EthCallClient } from './precompile'
|
||||
export {
|
||||
PRECOMPILE_MLDSA,
|
||||
PRECOMPILE_MLKEM,
|
||||
PRECOMPILE_SLHDSA,
|
||||
encodeMLDSAVerifyInput,
|
||||
encodeMLKEMVerifyInput,
|
||||
encodeSLHDSAVerifyInput,
|
||||
verifyMLDSAOnChain,
|
||||
} from './precompile'
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
import {
|
||||
mlDsa44Provider,
|
||||
mlDsa65Provider,
|
||||
mlDsa87Provider,
|
||||
providerFor,
|
||||
signWithContext,
|
||||
verifyWithContext,
|
||||
} from './mldsa'
|
||||
import { WalletSchemeID } from './pqAccount'
|
||||
|
||||
describe('ML-DSA providers', () => {
|
||||
describe('ML-DSA-44 (FIPS 204 cat 2)', () => {
|
||||
it('reports the FIPS-204 byte widths', () => {
|
||||
// FIPS 204 Table 1: ML-DSA-44 sizes (pk=1312, sk=2560, sig=2420).
|
||||
expect(mlDsa44Provider.publicKeySize).toBe(1312)
|
||||
expect(mlDsa44Provider.privateKeySize).toBe(2560)
|
||||
expect(mlDsa44Provider.signatureSize).toBe(2420)
|
||||
expect(mlDsa44Provider.schemeID).toBe(WalletSchemeID.MLDSA44)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ML-DSA-65 (FIPS 204 cat 3 — canonical)', () => {
|
||||
it('reports the FIPS-204 byte widths', () => {
|
||||
// FIPS 204 Table 1: ML-DSA-65 sizes (pk=1952, sk=4032, sig=3309).
|
||||
expect(mlDsa65Provider.publicKeySize).toBe(1952)
|
||||
expect(mlDsa65Provider.privateKeySize).toBe(4032)
|
||||
expect(mlDsa65Provider.signatureSize).toBe(3309)
|
||||
expect(mlDsa65Provider.schemeID).toBe(WalletSchemeID.MLDSA65)
|
||||
})
|
||||
|
||||
it('keygen → sign → verify round-trip', () => {
|
||||
const seed = new Uint8Array(32).fill(7)
|
||||
const { publicKey, privateKey } = mlDsa65Provider.newKeyFromSeed(seed)
|
||||
expect(publicKey.length).toBe(mlDsa65Provider.publicKeySize)
|
||||
expect(privateKey.length).toBe(mlDsa65Provider.privateKeySize)
|
||||
|
||||
const msg = new TextEncoder().encode('lux-pq-wallet-mldsa-65-roundtrip')
|
||||
const sig = signWithContext(mlDsa65Provider, privateKey, msg, 'evm-precompile')
|
||||
expect(sig.length).toBe(mlDsa65Provider.signatureSize)
|
||||
|
||||
const ok = verifyWithContext(mlDsa65Provider, publicKey, msg, sig, 'evm-precompile')
|
||||
expect(ok).toBe(true)
|
||||
})
|
||||
|
||||
it('verification fails under a different ctx — replay protection', () => {
|
||||
const seed = new Uint8Array(32).fill(9)
|
||||
const { publicKey, privateKey } = mlDsa65Provider.newKeyFromSeed(seed)
|
||||
const msg = new TextEncoder().encode('cross-chain-replay-attempt')
|
||||
const sig = signWithContext(mlDsa65Provider, privateKey, msg, 'evm-precompile')
|
||||
// A signature minted for the EVM precompile MUST NOT verify on
|
||||
// X-Chain UTXOs or the recovery path. This is the central
|
||||
// domain-separation property of FIPS 204 §5.4.
|
||||
expect(verifyWithContext(mlDsa65Provider, publicKey, msg, sig, 'x-chain-utxo')).toBe(false)
|
||||
expect(verifyWithContext(mlDsa65Provider, publicKey, msg, sig, 'p-chain-platform')).toBe(false)
|
||||
expect(verifyWithContext(mlDsa65Provider, publicKey, msg, sig, 'recovery')).toBe(false)
|
||||
})
|
||||
|
||||
it('verification fails for the wrong message', () => {
|
||||
const seed = new Uint8Array(32).fill(11)
|
||||
const { publicKey, privateKey } = mlDsa65Provider.newKeyFromSeed(seed)
|
||||
const sig = signWithContext(
|
||||
mlDsa65Provider,
|
||||
privateKey,
|
||||
new TextEncoder().encode('original'),
|
||||
'evm-precompile',
|
||||
)
|
||||
expect(
|
||||
verifyWithContext(
|
||||
mlDsa65Provider,
|
||||
publicKey,
|
||||
new TextEncoder().encode('tampered'),
|
||||
sig,
|
||||
'evm-precompile',
|
||||
),
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('deterministic keygen — same seed → same keypair', () => {
|
||||
const seed = new Uint8Array(32).fill(13)
|
||||
const a = mlDsa65Provider.newKeyFromSeed(seed)
|
||||
const b = mlDsa65Provider.newKeyFromSeed(seed)
|
||||
expect(a.publicKey).toEqual(b.publicKey)
|
||||
expect(a.privateKey).toEqual(b.privateKey)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ML-DSA-87 (FIPS 204 cat 5 — high-value)', () => {
|
||||
it('reports the FIPS-204 byte widths', () => {
|
||||
// FIPS 204 Table 1: ML-DSA-87 sizes (pk=2592, sk=4896, sig=4627).
|
||||
expect(mlDsa87Provider.publicKeySize).toBe(2592)
|
||||
expect(mlDsa87Provider.privateKeySize).toBe(4896)
|
||||
expect(mlDsa87Provider.signatureSize).toBe(4627)
|
||||
expect(mlDsa87Provider.schemeID).toBe(WalletSchemeID.MLDSA87)
|
||||
})
|
||||
})
|
||||
|
||||
describe('providerFor dispatch', () => {
|
||||
it('returns the canonical provider for each ML-DSA scheme', () => {
|
||||
expect(providerFor(WalletSchemeID.MLDSA44)).toBe(mlDsa44Provider)
|
||||
expect(providerFor(WalletSchemeID.MLDSA65)).toBe(mlDsa65Provider)
|
||||
expect(providerFor(WalletSchemeID.MLDSA87)).toBe(mlDsa87Provider)
|
||||
})
|
||||
|
||||
it('throws on a non-ML-DSA scheme byte', () => {
|
||||
expect(() => providerFor(WalletSchemeID.Secp256k1)).toThrow(/non-ML-DSA/)
|
||||
expect(() => providerFor(WalletSchemeID.None)).toThrow(/non-ML-DSA/)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
/**
|
||||
* Concrete ML-DSA providers backed by @noble/post-quantum.
|
||||
*
|
||||
* Wraps FIPS 204 (ml_dsa44 / ml_dsa65 / ml_dsa87) behind the
|
||||
* MLDSAProvider interface declared in pqAccount.ts. All signing /
|
||||
* verification paths through this module bind a domain-separator
|
||||
* context (FIPS 204 §5.4) so a signature produced for one verifier
|
||||
* surface (e.g. the EVM precompile at 0x012202) cannot be replayed
|
||||
* against another (e.g. X-Chain UTXOs).
|
||||
*
|
||||
* Threat-model boundary: the secretKey bytes accepted here are
|
||||
* sensitive material. Callers are responsible for sourcing them from
|
||||
* the auth slice (encrypted at rest, in RAM only while unlocked) and
|
||||
* MUST NOT log or persist them. This module performs no allocation
|
||||
* that outlives the call.
|
||||
*/
|
||||
|
||||
import { ml_dsa44, ml_dsa65, ml_dsa87 } from '@noble/post-quantum/ml-dsa.js'
|
||||
import {
|
||||
type MLDSAProvider,
|
||||
WalletSchemeID,
|
||||
} from './pqAccount'
|
||||
import { type SigningContext, contextBytesFor } from './domain'
|
||||
|
||||
/** Wrap one @noble/post-quantum ml_dsaN export as an MLDSAProvider. */
|
||||
function wrap(signer: typeof ml_dsa65, schemeID: WalletSchemeID): MLDSAProvider {
|
||||
// Lengths come from the noble signer's own `lengths` table — the only
|
||||
// source of truth for FIPS 204 byte widths in this codebase.
|
||||
const publicKeySize = signer.lengths.publicKey
|
||||
const secretKeySize = signer.lengths.secretKey
|
||||
const signatureSize = signer.lengths.signature
|
||||
if (typeof publicKeySize !== 'number') {
|
||||
throw new Error('pq/mldsa: noble ml_dsa lengths.publicKey is undefined')
|
||||
}
|
||||
if (typeof secretKeySize !== 'number') {
|
||||
throw new Error('pq/mldsa: noble ml_dsa lengths.secretKey is undefined')
|
||||
}
|
||||
if (typeof signatureSize !== 'number') {
|
||||
throw new Error('pq/mldsa: noble ml_dsa lengths.signature is undefined')
|
||||
}
|
||||
return {
|
||||
schemeID,
|
||||
publicKeySize,
|
||||
privateKeySize: secretKeySize,
|
||||
signatureSize,
|
||||
sign(privateKey, digest) {
|
||||
return signer.sign(digest, privateKey)
|
||||
},
|
||||
verify(publicKey, digest, signature) {
|
||||
return signer.verify(signature, digest, publicKey)
|
||||
},
|
||||
newKeyFromSeed(seed) {
|
||||
const { publicKey, secretKey } = signer.keygen(seed)
|
||||
return { publicKey, privateKey: secretKey }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** ML-DSA-44 provider (FIPS 204 category 2). Lightweight-compat path. */
|
||||
export const mlDsa44Provider: MLDSAProvider = wrap(ml_dsa44, WalletSchemeID.MLDSA44)
|
||||
|
||||
/** ML-DSA-65 provider (FIPS 204 category 3). Canonical PQ identity. */
|
||||
export const mlDsa65Provider: MLDSAProvider = wrap(ml_dsa65, WalletSchemeID.MLDSA65)
|
||||
|
||||
/** ML-DSA-87 provider (FIPS 204 category 5). High-value / root-key path. */
|
||||
export const mlDsa87Provider: MLDSAProvider = wrap(ml_dsa87, WalletSchemeID.MLDSA87)
|
||||
|
||||
/**
|
||||
* providerFor returns the canonical MLDSAProvider for a given scheme.
|
||||
* Throws on a non-ML-DSA scheme so misuse is loud rather than silent.
|
||||
*/
|
||||
export function providerFor(scheme: WalletSchemeID): MLDSAProvider {
|
||||
switch (scheme) {
|
||||
case WalletSchemeID.MLDSA44:
|
||||
return mlDsa44Provider
|
||||
case WalletSchemeID.MLDSA65:
|
||||
return mlDsa65Provider
|
||||
case WalletSchemeID.MLDSA87:
|
||||
return mlDsa87Provider
|
||||
default:
|
||||
throw new Error(
|
||||
`pq/mldsa: providerFor called with non-ML-DSA scheme 0x${scheme.toString(16)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* signWithContext signs a message under the FIPS-204 ctx parameter for
|
||||
* the named SigningContext. This is the primary entry point for any
|
||||
* wallet path that produces an ML-DSA signature that will be checked by
|
||||
* a Lux on-chain verifier.
|
||||
*
|
||||
* The message is passed through unchanged — the ctx prefix is applied
|
||||
* inside the noble signer per FIPS 204 §5.4.
|
||||
*/
|
||||
export function signWithContext(
|
||||
provider: MLDSAProvider,
|
||||
secretKey: Uint8Array,
|
||||
msg: Uint8Array,
|
||||
ctx: SigningContext,
|
||||
): Uint8Array {
|
||||
const inner = signerForScheme(provider.schemeID)
|
||||
return inner.sign(msg, secretKey, { context: contextBytesFor(ctx) })
|
||||
}
|
||||
|
||||
/**
|
||||
* verifyWithContext checks an ML-DSA signature under the same
|
||||
* SigningContext that the signer pinned. Returns false on any
|
||||
* verification failure; throws only on malformed API arguments.
|
||||
*/
|
||||
export function verifyWithContext(
|
||||
provider: MLDSAProvider,
|
||||
publicKey: Uint8Array,
|
||||
msg: Uint8Array,
|
||||
signature: Uint8Array,
|
||||
ctx: SigningContext,
|
||||
): boolean {
|
||||
const inner = signerForScheme(provider.schemeID)
|
||||
return inner.verify(signature, msg, publicKey, { context: contextBytesFor(ctx) })
|
||||
}
|
||||
|
||||
/** Dispatch back to the noble signer; kept internal. */
|
||||
function signerForScheme(scheme: WalletSchemeID): typeof ml_dsa65 {
|
||||
switch (scheme) {
|
||||
case WalletSchemeID.MLDSA44:
|
||||
return ml_dsa44
|
||||
case WalletSchemeID.MLDSA65:
|
||||
return ml_dsa65
|
||||
case WalletSchemeID.MLDSA87:
|
||||
return ml_dsa87
|
||||
default:
|
||||
throw new Error(
|
||||
`pq/mldsa: signerForScheme called with non-ML-DSA scheme 0x${scheme.toString(16)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
import {
|
||||
kemProviderByName,
|
||||
mlKem1024Provider,
|
||||
mlKem512Provider,
|
||||
mlKem768Provider,
|
||||
} from './mlkem'
|
||||
|
||||
describe('ML-KEM providers', () => {
|
||||
describe('ML-KEM-768 (FIPS 203 cat 3 — canonical)', () => {
|
||||
it('reports the FIPS-203 byte widths', () => {
|
||||
// FIPS 203 Table 3 row ML-KEM-768: pk=1184, sk=2400, ct=1088, ss=32.
|
||||
expect(mlKem768Provider.publicKeySize).toBe(1184)
|
||||
expect(mlKem768Provider.privateKeySize).toBe(2400)
|
||||
expect(mlKem768Provider.ciphertextSize).toBe(1088)
|
||||
expect(mlKem768Provider.sharedSecretSize).toBe(32)
|
||||
})
|
||||
|
||||
it('keygen → encaps → decaps round-trip', () => {
|
||||
const { publicKey, privateKey } = mlKem768Provider.keygen()
|
||||
expect(publicKey.length).toBe(mlKem768Provider.publicKeySize)
|
||||
expect(privateKey.length).toBe(mlKem768Provider.privateKeySize)
|
||||
|
||||
const { ciphertext, sharedSecret } = mlKem768Provider.encapsulate(publicKey)
|
||||
expect(ciphertext.length).toBe(mlKem768Provider.ciphertextSize)
|
||||
expect(sharedSecret.length).toBe(32)
|
||||
|
||||
const recovered = mlKem768Provider.decapsulate(ciphertext, privateKey)
|
||||
expect(recovered).toEqual(sharedSecret)
|
||||
})
|
||||
|
||||
it('decaps under wrong sk produces a different (implicit-reject) secret', () => {
|
||||
const a = mlKem768Provider.keygen()
|
||||
const b = mlKem768Provider.keygen()
|
||||
const { ciphertext, sharedSecret } = mlKem768Provider.encapsulate(a.publicKey)
|
||||
const wrong = mlKem768Provider.decapsulate(ciphertext, b.privateKey)
|
||||
// FIPS 203 implicit rejection: decap with the wrong sk returns a
|
||||
// pseudorandom secret derived from a sk-bound rejection hash, NOT
|
||||
// an error. The returned bytes MUST differ from the original.
|
||||
expect(wrong).not.toEqual(sharedSecret)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ML-KEM-512', () => {
|
||||
it('reports the FIPS-203 byte widths', () => {
|
||||
expect(mlKem512Provider.publicKeySize).toBe(800)
|
||||
expect(mlKem512Provider.privateKeySize).toBe(1632)
|
||||
expect(mlKem512Provider.ciphertextSize).toBe(768)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ML-KEM-1024', () => {
|
||||
it('reports the FIPS-203 byte widths', () => {
|
||||
expect(mlKem1024Provider.publicKeySize).toBe(1568)
|
||||
expect(mlKem1024Provider.privateKeySize).toBe(3168)
|
||||
expect(mlKem1024Provider.ciphertextSize).toBe(1568)
|
||||
})
|
||||
})
|
||||
|
||||
describe('kemProviderByName dispatch', () => {
|
||||
it('returns the canonical provider by name', () => {
|
||||
expect(kemProviderByName('ml-kem-512')).toBe(mlKem512Provider)
|
||||
expect(kemProviderByName('ml-kem-768')).toBe(mlKem768Provider)
|
||||
expect(kemProviderByName('ml-kem-1024')).toBe(mlKem1024Provider)
|
||||
})
|
||||
|
||||
it('throws on unknown name', () => {
|
||||
expect(() => kemProviderByName('not-a-kem')).toThrow(/unknown KEM/)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,98 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
/**
|
||||
* ML-KEM (FIPS 203) key-encapsulation wrappers.
|
||||
*
|
||||
* Used by the wallet's PQ-TLS hybrid handshake (X25519 || ML-KEM-768 in
|
||||
* TLS 1.3 hybrid_kem) and by the encrypted-DM channel for wallet-to-
|
||||
* wallet messaging. The provider surface mirrors the on-chain
|
||||
* precompile at 0x012201 (LP-4200 ML-KEM verifier) so the wallet can
|
||||
* call eth_call with the same wire shape used here.
|
||||
*
|
||||
* Wraps the @noble/post-quantum/ml-kem exports. No custom math: the
|
||||
* lib is the reference implementation we depend on.
|
||||
*/
|
||||
|
||||
import { ml_kem512, ml_kem768, ml_kem1024 } from '@noble/post-quantum/ml-kem.js'
|
||||
import { WalletSchemeID } from './pqAccount'
|
||||
|
||||
/**
|
||||
* KEMProvider is the wallet-side capability for one ML-KEM parameter
|
||||
* set. encapsulate produces (ciphertext, sharedSecret) under the
|
||||
* recipient's public key; decapsulate recovers the shared secret from
|
||||
* a ciphertext under the recipient's secret key.
|
||||
*
|
||||
* Lengths are pinned at construction time so a caller can validate
|
||||
* inputs without round-tripping through the noble lib.
|
||||
*/
|
||||
export interface KEMProvider {
|
||||
readonly name: string
|
||||
readonly publicKeySize: number
|
||||
readonly privateKeySize: number
|
||||
readonly ciphertextSize: number
|
||||
readonly sharedSecretSize: 32
|
||||
|
||||
keygen(seed?: Uint8Array): { publicKey: Uint8Array; privateKey: Uint8Array }
|
||||
encapsulate(publicKey: Uint8Array): { ciphertext: Uint8Array; sharedSecret: Uint8Array }
|
||||
decapsulate(ciphertext: Uint8Array, privateKey: Uint8Array): Uint8Array
|
||||
}
|
||||
|
||||
function wrapKem(kem: typeof ml_kem768, name: string): KEMProvider {
|
||||
const publicKeySize = kem.lengths.publicKey
|
||||
const secretKeySize = kem.lengths.secretKey
|
||||
const ciphertextSize = kem.lengths.cipherText
|
||||
if (typeof publicKeySize !== 'number') {
|
||||
throw new Error(`pq/mlkem: noble ${name} lengths.publicKey is undefined`)
|
||||
}
|
||||
if (typeof secretKeySize !== 'number') {
|
||||
throw new Error(`pq/mlkem: noble ${name} lengths.secretKey is undefined`)
|
||||
}
|
||||
if (typeof ciphertextSize !== 'number') {
|
||||
throw new Error(`pq/mlkem: noble ${name} lengths.cipherText is undefined`)
|
||||
}
|
||||
return {
|
||||
name,
|
||||
publicKeySize,
|
||||
privateKeySize: secretKeySize,
|
||||
ciphertextSize,
|
||||
sharedSecretSize: 32,
|
||||
keygen(seed) {
|
||||
const { publicKey, secretKey } = kem.keygen(seed)
|
||||
return { publicKey, privateKey: secretKey }
|
||||
},
|
||||
encapsulate(publicKey) {
|
||||
const { cipherText, sharedSecret } = kem.encapsulate(publicKey)
|
||||
return { ciphertext: cipherText, sharedSecret }
|
||||
},
|
||||
decapsulate(ciphertext, privateKey) {
|
||||
return kem.decapsulate(ciphertext, privateKey)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** ML-KEM-512 (FIPS 203 category 1). Lightweight handshake. */
|
||||
export const mlKem512Provider: KEMProvider = wrapKem(ml_kem512, 'ml-kem-512')
|
||||
|
||||
/** ML-KEM-768 (FIPS 203 category 3). Canonical KEM for PQ-TLS / encrypted-DM. */
|
||||
export const mlKem768Provider: KEMProvider = wrapKem(ml_kem768, 'ml-kem-768')
|
||||
|
||||
/** ML-KEM-1024 (FIPS 203 category 5). High-value channel. */
|
||||
export const mlKem1024Provider: KEMProvider = wrapKem(ml_kem1024, 'ml-kem-1024')
|
||||
|
||||
/** Convenience: the canonical KEM scheme byte (mirrors the Go-side enum). */
|
||||
export const WALLET_KEM_SCHEME_MLKEM768 = WalletSchemeID.MLDSA65 // re-used as a marker; KEM has no scheme byte today
|
||||
|
||||
/** Look up a KEM provider by lowercase canonical name. */
|
||||
export function kemProviderByName(name: string): KEMProvider {
|
||||
switch (name) {
|
||||
case 'ml-kem-512':
|
||||
return mlKem512Provider
|
||||
case 'ml-kem-768':
|
||||
return mlKem768Provider
|
||||
case 'ml-kem-1024':
|
||||
return mlKem1024Provider
|
||||
default:
|
||||
throw new Error(`pq/mlkem: unknown KEM provider ${name}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
import {
|
||||
PRECOMPILE_MLDSA,
|
||||
PRECOMPILE_MLKEM,
|
||||
PRECOMPILE_SLHDSA,
|
||||
encodeMLDSAVerifyInput,
|
||||
encodeMLKEMVerifyInput,
|
||||
encodeSLHDSAVerifyInput,
|
||||
verifyMLDSAOnChain,
|
||||
} from './precompile'
|
||||
import { mlDsa65Provider, signWithContext } from './mldsa'
|
||||
import { WalletSchemeID } from './pqAccount'
|
||||
import { PQ_CONTEXT_EVM_PRECOMPILE_MLDSA } from './domain'
|
||||
|
||||
describe('LP-4200 PQ precompile encoders', () => {
|
||||
it('pins the canonical precompile addresses', () => {
|
||||
expect(PRECOMPILE_MLKEM).toBe('0x0000000000000000000000000000000000012201')
|
||||
expect(PRECOMPILE_MLDSA).toBe('0x0000000000000000000000000000000000012202')
|
||||
expect(PRECOMPILE_SLHDSA).toBe('0x0000000000000000000000000000000000012203')
|
||||
})
|
||||
|
||||
describe('encodeMLDSAVerifyInput', () => {
|
||||
it('produces the canonical layout: scheme || ctx_len || ctx || pk_len || pk || sig_len || sig || msg', () => {
|
||||
const seed = new Uint8Array(32).fill(7)
|
||||
const { publicKey, privateKey } = mlDsa65Provider.newKeyFromSeed(seed)
|
||||
const msg = new TextEncoder().encode('on-chain-verify-target')
|
||||
const signature = signWithContext(mlDsa65Provider, privateKey, msg, 'evm-precompile')
|
||||
|
||||
const input = encodeMLDSAVerifyInput({
|
||||
scheme: WalletSchemeID.MLDSA65,
|
||||
ctx: 'evm-precompile',
|
||||
publicKey,
|
||||
signature,
|
||||
msg,
|
||||
})
|
||||
|
||||
// First byte = scheme = 0x42 (ML-DSA-65).
|
||||
expect(input[0]).toBe(0x42)
|
||||
// Second byte = ctx length = len("lux-evm-precompile-mldsa-v1") = 27.
|
||||
expect(input[1]).toBe(PQ_CONTEXT_EVM_PRECOMPILE_MLDSA.length)
|
||||
// Then ctx bytes.
|
||||
const ctxSlice = input.slice(2, 2 + 27)
|
||||
expect(new TextDecoder().decode(ctxSlice)).toBe(PQ_CONTEXT_EVM_PRECOMPILE_MLDSA)
|
||||
// Then 2-byte BE pk length = 1952.
|
||||
const pkLen = (input[2 + 27] << 8) | input[2 + 27 + 1]
|
||||
expect(pkLen).toBe(mlDsa65Provider.publicKeySize)
|
||||
// Then pk bytes.
|
||||
const pkStart = 2 + 27 + 2
|
||||
const pkSlice = input.slice(pkStart, pkStart + pkLen)
|
||||
expect(pkSlice).toEqual(publicKey)
|
||||
// Then 3-byte BE sig length = 3309.
|
||||
const sigLenStart = pkStart + pkLen
|
||||
const sigLen =
|
||||
(input[sigLenStart] << 16) |
|
||||
(input[sigLenStart + 1] << 8) |
|
||||
input[sigLenStart + 2]
|
||||
expect(sigLen).toBe(mlDsa65Provider.signatureSize)
|
||||
// Then sig bytes.
|
||||
const sigStart = sigLenStart + 3
|
||||
const sigSlice = input.slice(sigStart, sigStart + sigLen)
|
||||
expect(sigSlice).toEqual(signature)
|
||||
// Then msg.
|
||||
const msgSlice = input.slice(sigStart + sigLen)
|
||||
expect(msgSlice).toEqual(msg)
|
||||
})
|
||||
|
||||
it('rejects non-ML-DSA scheme bytes', () => {
|
||||
expect(() =>
|
||||
encodeMLDSAVerifyInput({
|
||||
scheme: WalletSchemeID.Secp256k1,
|
||||
ctx: 'evm-precompile',
|
||||
publicKey: new Uint8Array(1),
|
||||
signature: new Uint8Array(1),
|
||||
msg: new Uint8Array(1),
|
||||
}),
|
||||
).toThrow(/not ML-DSA/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('encodeMLKEMVerifyInput', () => {
|
||||
it('produces the canonical layout: variant || pk_len || pk || ct_len || ct || ss_len || ss', () => {
|
||||
const pk = new Uint8Array(1184).fill(0xa1)
|
||||
const ct = new Uint8Array(1088).fill(0xb2)
|
||||
const ss = new Uint8Array(32).fill(0xc3)
|
||||
const input = encodeMLKEMVerifyInput({ variant: 2, publicKey: pk, ciphertext: ct, sharedSecret: ss })
|
||||
expect(input[0]).toBe(0x02)
|
||||
const pkLen = (input[1] << 8) | input[2]
|
||||
expect(pkLen).toBe(1184)
|
||||
const ctLen = (input[3 + 1184] << 8) | input[3 + 1184 + 1]
|
||||
expect(ctLen).toBe(1088)
|
||||
const ssLen = input[3 + 1184 + 2 + 1088]
|
||||
expect(ssLen).toBe(32)
|
||||
})
|
||||
})
|
||||
|
||||
describe('encodeSLHDSAVerifyInput', () => {
|
||||
it('produces a non-empty input buffer', () => {
|
||||
const input = encodeSLHDSAVerifyInput({
|
||||
paramSet: 0x09,
|
||||
ctx: 'recovery',
|
||||
publicKey: new Uint8Array(48),
|
||||
signature: new Uint8Array(35664),
|
||||
msg: new TextEncoder().encode('recovery-target'),
|
||||
})
|
||||
// paramSet(1) + ctx_len(1) + ctx(22 for 'lux-recovery-slhdsa-v1') + pk_len(2)
|
||||
// + pk(48) + sig_len(3) + sig(35664) + msg(15) = 35756.
|
||||
expect(input.length).toBe(1 + 1 + 22 + 2 + 48 + 3 + 35664 + 15)
|
||||
expect(input[0]).toBe(0x09)
|
||||
expect(input[1]).toBe(22)
|
||||
})
|
||||
})
|
||||
|
||||
describe('verifyMLDSAOnChain (eth_call wrapper)', () => {
|
||||
it('dispatches to the canonical precompile address', async () => {
|
||||
const calls: Array<{ to: string; data: string }> = []
|
||||
const mockClient = {
|
||||
async call(input: { to: `0x${string}`; data: `0x${string}` }) {
|
||||
calls.push(input)
|
||||
// Precompile returns 0x...01 on valid.
|
||||
return ('0x' + '00'.repeat(31) + '01') as `0x${string}`
|
||||
},
|
||||
}
|
||||
const seed = new Uint8Array(32).fill(11)
|
||||
const { publicKey, privateKey } = mlDsa65Provider.newKeyFromSeed(seed)
|
||||
const msg = new TextEncoder().encode('on-chain-call')
|
||||
const sig = signWithContext(mlDsa65Provider, privateKey, msg, 'evm-precompile')
|
||||
|
||||
const ok = await verifyMLDSAOnChain(mockClient, {
|
||||
scheme: WalletSchemeID.MLDSA65, ctx: 'evm-precompile', publicKey, signature: sig, msg,
|
||||
})
|
||||
expect(ok).toBe(true)
|
||||
expect(calls.length).toBe(1)
|
||||
expect(calls[0].to).toBe(PRECOMPILE_MLDSA)
|
||||
// Input prefixes the scheme byte 0x42 right after "0x".
|
||||
expect(calls[0].data.startsWith('0x42')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when the precompile returns zero', async () => {
|
||||
const mockClient = {
|
||||
async call(_input: { to: `0x${string}`; data: `0x${string}` }) {
|
||||
return ('0x' + '00'.repeat(32)) as `0x${string}`
|
||||
},
|
||||
}
|
||||
const seed = new Uint8Array(32).fill(13)
|
||||
const { publicKey, privateKey } = mlDsa65Provider.newKeyFromSeed(seed)
|
||||
const msg = new TextEncoder().encode('not-on-chain')
|
||||
const sig = signWithContext(mlDsa65Provider, privateKey, msg, 'evm-precompile')
|
||||
const ok = await verifyMLDSAOnChain(mockClient, {
|
||||
scheme: WalletSchemeID.MLDSA65, ctx: 'evm-precompile', publicKey, signature: sig, msg,
|
||||
})
|
||||
expect(ok).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,196 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
/**
|
||||
* On-chain PQ precompile call wrappers (LP-4200).
|
||||
*
|
||||
* The Lux C-Chain EVM exposes three precompiles for post-quantum
|
||||
* verification:
|
||||
*
|
||||
* 0x012201 — ML-KEM verify (FIPS 203)
|
||||
* 0x012202 — ML-DSA verify (FIPS 204)
|
||||
* 0x012203 — SLH-DSA verify (FIPS 205)
|
||||
*
|
||||
* Each precompile is profile-gated by `contract.RefuseUnderStrictPQ` on
|
||||
* the node side: a single function in a single place gates whether
|
||||
* classical-mode calls are accepted. The wallet's job is to construct
|
||||
* the input bytes in the canonical layout and dispatch an eth_call.
|
||||
*
|
||||
* Input layout for 0x012202 (ML-DSA verify):
|
||||
*
|
||||
* scheme_byte(1) || ctx_len(1) || ctx(ctx_len) || pk_len(2 BE) || pk || sig_len(3 BE) || sig || msg
|
||||
*
|
||||
* scheme_byte ∈ {0x41=ML-DSA-44, 0x42=ML-DSA-65, 0x43=ML-DSA-87}
|
||||
* matching WalletSchemeID. Output: 32-byte word, 0x...01 = valid,
|
||||
* 0x...00 = invalid. This is identical to the precompile-side decoder
|
||||
* in luxfi/node contract/precompile/pq/mldsa.go.
|
||||
*
|
||||
* The wallet never invokes a precompile to *produce* a signature — it
|
||||
* only calls eth_call to *verify* a signature it just produced, as a
|
||||
* deployment-time sanity check / acceptance test. Real on-chain
|
||||
* verification happens when the wallet submits a tx and the EVM runs
|
||||
* the verifier as part of the auth check.
|
||||
*/
|
||||
|
||||
import { type SigningContext, contextBytesFor } from './domain'
|
||||
import { WalletSchemeID } from './pqAccount'
|
||||
|
||||
/** LP-4200 precompile addresses (Lux C-Chain). */
|
||||
export const PRECOMPILE_MLKEM = '0x0000000000000000000000000000000000012201' as const
|
||||
export const PRECOMPILE_MLDSA = '0x0000000000000000000000000000000000012202' as const
|
||||
export const PRECOMPILE_SLHDSA = '0x0000000000000000000000000000000000012203' as const
|
||||
|
||||
/** Minimal eth_call client surface — concrete clients (viem, ethers, fetch) plug in. */
|
||||
export interface EthCallClient {
|
||||
call(input: { to: `0x${string}`; data: `0x${string}` }): Promise<`0x${string}`>
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode the input bytes for the ML-DSA verify precompile.
|
||||
*
|
||||
* Layout: scheme(1) || ctx_len(1) || ctx(ctx_len) || pk_len(2 BE) || pk
|
||||
* || sig_len(3 BE) || sig || msg
|
||||
*
|
||||
* The sig length is 3 BE bytes because ML-DSA-87 signatures are ~4627
|
||||
* bytes — exceeds u16. Two BE bytes for pk is sufficient (ML-DSA-87 pk
|
||||
* is 2592 bytes).
|
||||
*/
|
||||
export function encodeMLDSAVerifyInput(opts: {
|
||||
scheme: WalletSchemeID
|
||||
ctx: SigningContext
|
||||
publicKey: Uint8Array
|
||||
signature: Uint8Array
|
||||
msg: Uint8Array
|
||||
}): Uint8Array {
|
||||
if (opts.scheme !== WalletSchemeID.MLDSA44 &&
|
||||
opts.scheme !== WalletSchemeID.MLDSA65 &&
|
||||
opts.scheme !== WalletSchemeID.MLDSA87) {
|
||||
throw new Error(`pq/precompile: scheme 0x${opts.scheme.toString(16)} is not ML-DSA`)
|
||||
}
|
||||
const ctx = contextBytesFor(opts.ctx)
|
||||
if (ctx.length > 0xff) {
|
||||
throw new Error(`pq/precompile: ctx length ${ctx.length} exceeds 0xff`)
|
||||
}
|
||||
if (opts.publicKey.length > 0xffff) {
|
||||
throw new Error(`pq/precompile: pk length ${opts.publicKey.length} exceeds 0xffff`)
|
||||
}
|
||||
if (opts.signature.length > 0xffffff) {
|
||||
throw new Error(`pq/precompile: sig length ${opts.signature.length} exceeds 0xffffff`)
|
||||
}
|
||||
const total =
|
||||
1 + 1 + ctx.length + 2 + opts.publicKey.length + 3 + opts.signature.length + opts.msg.length
|
||||
const out = new Uint8Array(total)
|
||||
let o = 0
|
||||
out[o++] = opts.scheme
|
||||
out[o++] = ctx.length
|
||||
out.set(ctx, o); o += ctx.length
|
||||
out[o++] = (opts.publicKey.length >> 8) & 0xff
|
||||
out[o++] = opts.publicKey.length & 0xff
|
||||
out.set(opts.publicKey, o); o += opts.publicKey.length
|
||||
out[o++] = (opts.signature.length >> 16) & 0xff
|
||||
out[o++] = (opts.signature.length >> 8) & 0xff
|
||||
out[o++] = opts.signature.length & 0xff
|
||||
out.set(opts.signature, o); o += opts.signature.length
|
||||
out.set(opts.msg, o); o += opts.msg.length
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify an ML-DSA signature by eth_call against the LP-4200 precompile.
|
||||
*
|
||||
* Returns true iff the precompile returns a non-zero 32-byte word.
|
||||
* Network failures throw — callers should distinguish that from a
|
||||
* verification failure via try/catch.
|
||||
*/
|
||||
export async function verifyMLDSAOnChain(client: EthCallClient, opts: {
|
||||
scheme: WalletSchemeID
|
||||
ctx: SigningContext
|
||||
publicKey: Uint8Array
|
||||
signature: Uint8Array
|
||||
msg: Uint8Array
|
||||
}): Promise<boolean> {
|
||||
const input = encodeMLDSAVerifyInput(opts)
|
||||
const data = `0x${bytesToHex(input)}` as `0x${string}`
|
||||
const ret = await client.call({ to: PRECOMPILE_MLDSA, data })
|
||||
return retIsTrue(ret)
|
||||
}
|
||||
|
||||
/** Encode the input bytes for the ML-KEM verify precompile.
|
||||
*
|
||||
* Layout: kem_variant(1) || pk_len(2 BE) || pk || ct_len(2 BE) || ct || ss_len(1) || ss
|
||||
*
|
||||
* The precompile recomputes encapsulation under (pk, opts) and returns
|
||||
* true iff the supplied shared secret matches — this is a re-encaps
|
||||
* proof, not a generic ML-KEM oracle. variant ∈ {0x01=ML-KEM-512,
|
||||
* 0x02=ML-KEM-768, 0x03=ML-KEM-1024}.
|
||||
*/
|
||||
export function encodeMLKEMVerifyInput(opts: {
|
||||
variant: 1 | 2 | 3
|
||||
publicKey: Uint8Array
|
||||
ciphertext: Uint8Array
|
||||
sharedSecret: Uint8Array
|
||||
}): Uint8Array {
|
||||
if (opts.publicKey.length > 0xffff) throw new Error('pq/precompile: pk exceeds u16')
|
||||
if (opts.ciphertext.length > 0xffff) throw new Error('pq/precompile: ct exceeds u16')
|
||||
if (opts.sharedSecret.length > 0xff) throw new Error('pq/precompile: ss exceeds u8')
|
||||
const total = 1 + 2 + opts.publicKey.length + 2 + opts.ciphertext.length + 1 + opts.sharedSecret.length
|
||||
const out = new Uint8Array(total)
|
||||
let o = 0
|
||||
out[o++] = opts.variant
|
||||
out[o++] = (opts.publicKey.length >> 8) & 0xff
|
||||
out[o++] = opts.publicKey.length & 0xff
|
||||
out.set(opts.publicKey, o); o += opts.publicKey.length
|
||||
out[o++] = (opts.ciphertext.length >> 8) & 0xff
|
||||
out[o++] = opts.ciphertext.length & 0xff
|
||||
out.set(opts.ciphertext, o); o += opts.ciphertext.length
|
||||
out[o++] = opts.sharedSecret.length
|
||||
out.set(opts.sharedSecret, o); o += opts.sharedSecret.length
|
||||
return out
|
||||
}
|
||||
|
||||
/** Encode the input bytes for the SLH-DSA verify precompile. */
|
||||
export function encodeSLHDSAVerifyInput(opts: {
|
||||
paramSet: number // matches FIPS-205 row index; e.g. 0x09 = sha2-192f
|
||||
ctx: SigningContext
|
||||
publicKey: Uint8Array
|
||||
signature: Uint8Array
|
||||
msg: Uint8Array
|
||||
}): Uint8Array {
|
||||
const ctx = contextBytesFor(opts.ctx)
|
||||
if (ctx.length > 0xff) throw new Error('pq/precompile: ctx exceeds u8')
|
||||
if (opts.publicKey.length > 0xffff) throw new Error('pq/precompile: pk exceeds u16')
|
||||
if (opts.signature.length > 0xffffff) throw new Error('pq/precompile: sig exceeds u24')
|
||||
const total =
|
||||
1 + 1 + ctx.length + 2 + opts.publicKey.length + 3 + opts.signature.length + opts.msg.length
|
||||
const out = new Uint8Array(total)
|
||||
let o = 0
|
||||
out[o++] = opts.paramSet
|
||||
out[o++] = ctx.length
|
||||
out.set(ctx, o); o += ctx.length
|
||||
out[o++] = (opts.publicKey.length >> 8) & 0xff
|
||||
out[o++] = opts.publicKey.length & 0xff
|
||||
out.set(opts.publicKey, o); o += opts.publicKey.length
|
||||
out[o++] = (opts.signature.length >> 16) & 0xff
|
||||
out[o++] = (opts.signature.length >> 8) & 0xff
|
||||
out[o++] = opts.signature.length & 0xff
|
||||
out.set(opts.signature, o); o += opts.signature.length
|
||||
out.set(opts.msg, o); o += opts.msg.length
|
||||
return out
|
||||
}
|
||||
|
||||
function bytesToHex(b: Uint8Array): string {
|
||||
let s = ''
|
||||
for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, '0')
|
||||
return s
|
||||
}
|
||||
|
||||
function retIsTrue(ret: string): boolean {
|
||||
// 32-byte ABI bool: 0x0..01 = true.
|
||||
const v = ret.startsWith('0x') ? ret.slice(2) : ret
|
||||
if (v.length === 0) return false
|
||||
// Any non-zero byte counts as true under EVM bool semantics.
|
||||
for (let i = 0; i < v.length; i += 2) {
|
||||
if (v[i] !== '0' || v[i + 1] !== '0') return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
import {
|
||||
slhDsa128fProvider,
|
||||
slhDsa192fProvider,
|
||||
slhDsa192sProvider,
|
||||
slhDsaProviderByName,
|
||||
} from './slhdsa'
|
||||
|
||||
// SLH-DSA-192f signing is slow (~hundreds of ms in JS). We keep this
|
||||
// suite minimal — a single round-trip plus the parameter-set checks.
|
||||
// The byte-width checks are cheap and run unconditionally.
|
||||
|
||||
describe('SLH-DSA providers', () => {
|
||||
describe('SLH-DSA-SHA2-192f (FIPS 205 cat 3 — canonical recovery)', () => {
|
||||
it('reports the FIPS-205 byte widths', () => {
|
||||
// FIPS 205 Table 2 row SLH-DSA-SHA2-192f: pk=48, sk=96, sig=35664.
|
||||
expect(slhDsa192fProvider.publicKeySize).toBe(48)
|
||||
expect(slhDsa192fProvider.privateKeySize).toBe(96)
|
||||
expect(slhDsa192fProvider.signatureSize).toBe(35664)
|
||||
expect(slhDsa192fProvider.name).toBe('slh-dsa-sha2-192f')
|
||||
})
|
||||
|
||||
it('keygen → sign → verify round-trip under the recovery ctx', () => {
|
||||
const { publicKey, privateKey } = slhDsa192fProvider.keygen()
|
||||
expect(publicKey.length).toBe(slhDsa192fProvider.publicKeySize)
|
||||
expect(privateKey.length).toBe(slhDsa192fProvider.privateKeySize)
|
||||
|
||||
const msg = new TextEncoder().encode('lux-pq-wallet-slhdsa-192f-recovery')
|
||||
const sig = slhDsa192fProvider.sign(privateKey, msg, 'recovery')
|
||||
expect(sig.length).toBe(slhDsa192fProvider.signatureSize)
|
||||
|
||||
const ok = slhDsa192fProvider.verify(publicKey, msg, sig, 'recovery')
|
||||
expect(ok).toBe(true)
|
||||
}, 60000) // SLH-DSA-192f signing is slow; allow 60s.
|
||||
})
|
||||
|
||||
describe('SLH-DSA-SHA2-128f', () => {
|
||||
it('reports the FIPS-205 byte widths', () => {
|
||||
// FIPS 205 Table 2 row SLH-DSA-SHA2-128f: pk=32, sk=64, sig=17088.
|
||||
expect(slhDsa128fProvider.publicKeySize).toBe(32)
|
||||
expect(slhDsa128fProvider.privateKeySize).toBe(64)
|
||||
expect(slhDsa128fProvider.signatureSize).toBe(17088)
|
||||
})
|
||||
})
|
||||
|
||||
describe('SLH-DSA-SHA2-192s', () => {
|
||||
it('reports the FIPS-205 small-variant byte widths', () => {
|
||||
// FIPS 205 Table 2 row SLH-DSA-SHA2-192s: pk=48, sk=96, sig=16224.
|
||||
expect(slhDsa192sProvider.publicKeySize).toBe(48)
|
||||
expect(slhDsa192sProvider.privateKeySize).toBe(96)
|
||||
expect(slhDsa192sProvider.signatureSize).toBe(16224)
|
||||
})
|
||||
})
|
||||
|
||||
describe('slhDsaProviderByName dispatch', () => {
|
||||
it('returns the canonical provider by name', () => {
|
||||
expect(slhDsaProviderByName('slh-dsa-sha2-128f')).toBe(slhDsa128fProvider)
|
||||
expect(slhDsaProviderByName('slh-dsa-sha2-192f')).toBe(slhDsa192fProvider)
|
||||
expect(slhDsaProviderByName('slh-dsa-sha2-192s')).toBe(slhDsa192sProvider)
|
||||
})
|
||||
|
||||
it('throws on unknown name', () => {
|
||||
expect(() => slhDsaProviderByName('not-a-sig')).toThrow(/unknown SLH-DSA/)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
/**
|
||||
* SLH-DSA (FIPS 205) hash-based signatures.
|
||||
*
|
||||
* Used for cold-storage / recovery: SLH-DSA-192f is the canonical
|
||||
* recovery parameter set per LP-4200. Hash-based signatures are
|
||||
* conservative — security rests only on the underlying hash family,
|
||||
* not on a structured assumption (LWE, MLWE, discrete log). The
|
||||
* trade-off is large signatures (~17 KB for 192f) and slow signing,
|
||||
* which is acceptable for an offline, single-use recovery flow.
|
||||
*
|
||||
* The wallet generates the recovery seed once, signs the recovery
|
||||
* envelope offline (paper / hardware), and never re-signs with the
|
||||
* same key. The on-chain verifier at 0x012203 checks the SLH-DSA
|
||||
* signature against the published public key.
|
||||
*/
|
||||
|
||||
import {
|
||||
slh_dsa_sha2_128f,
|
||||
slh_dsa_sha2_192f,
|
||||
slh_dsa_sha2_192s,
|
||||
slh_dsa_sha2_256f,
|
||||
} from '@noble/post-quantum/slh-dsa.js'
|
||||
import { type SigningContext, contextBytesFor } from './domain'
|
||||
|
||||
/**
|
||||
* SLHDSAProvider wraps one FIPS 205 parameter set behind a uniform
|
||||
* surface. The wallet only generates a key, signs once, and verifies;
|
||||
* no in-memory long-lived private key (the recovery key is meant to
|
||||
* live offline / on paper).
|
||||
*/
|
||||
export interface SLHDSAProvider {
|
||||
readonly name: string
|
||||
readonly publicKeySize: number
|
||||
readonly privateKeySize: number
|
||||
readonly signatureSize: number
|
||||
|
||||
keygen(seed?: Uint8Array): { publicKey: Uint8Array; privateKey: Uint8Array }
|
||||
sign(privateKey: Uint8Array, msg: Uint8Array, ctx: SigningContext): Uint8Array
|
||||
verify(publicKey: Uint8Array, msg: Uint8Array, signature: Uint8Array, ctx: SigningContext): boolean
|
||||
}
|
||||
|
||||
function wrapSphincs(signer: typeof slh_dsa_sha2_192f, name: string): SLHDSAProvider {
|
||||
const publicKeySize = signer.lengths.publicKey
|
||||
const secretKeySize = signer.lengths.secretKey
|
||||
const signatureSize = signer.lengths.signature
|
||||
if (typeof publicKeySize !== 'number') {
|
||||
throw new Error(`pq/slhdsa: noble ${name} lengths.publicKey is undefined`)
|
||||
}
|
||||
if (typeof secretKeySize !== 'number') {
|
||||
throw new Error(`pq/slhdsa: noble ${name} lengths.secretKey is undefined`)
|
||||
}
|
||||
if (typeof signatureSize !== 'number') {
|
||||
throw new Error(`pq/slhdsa: noble ${name} lengths.signature is undefined`)
|
||||
}
|
||||
return {
|
||||
name,
|
||||
publicKeySize,
|
||||
privateKeySize: secretKeySize,
|
||||
signatureSize,
|
||||
keygen(seed) {
|
||||
const { publicKey, secretKey } = signer.keygen(seed)
|
||||
return { publicKey, privateKey: secretKey }
|
||||
},
|
||||
sign(privateKey, msg, ctx) {
|
||||
return signer.sign(msg, privateKey, { context: contextBytesFor(ctx) })
|
||||
},
|
||||
verify(publicKey, msg, signature, ctx) {
|
||||
return signer.verify(signature, msg, publicKey, { context: contextBytesFor(ctx) })
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** SLH-DSA-SHA2-128f (FIPS 205 category 1). Lightweight cold storage. */
|
||||
export const slhDsa128fProvider: SLHDSAProvider = wrapSphincs(slh_dsa_sha2_128f, 'slh-dsa-sha2-128f')
|
||||
|
||||
/** SLH-DSA-SHA2-192f (FIPS 205 category 3). Canonical recovery signature. */
|
||||
export const slhDsa192fProvider: SLHDSAProvider = wrapSphincs(slh_dsa_sha2_192f, 'slh-dsa-sha2-192f')
|
||||
|
||||
/** SLH-DSA-SHA2-192s (FIPS 205 category 3, small variant). Slower sign, smaller signature. */
|
||||
export const slhDsa192sProvider: SLHDSAProvider = wrapSphincs(slh_dsa_sha2_192s, 'slh-dsa-sha2-192s')
|
||||
|
||||
/** SLH-DSA-SHA2-256f (FIPS 205 category 5). High-value cold storage. */
|
||||
export const slhDsa256fProvider: SLHDSAProvider = wrapSphincs(slh_dsa_sha2_256f, 'slh-dsa-sha2-256f')
|
||||
|
||||
/** Look up an SLH-DSA provider by lowercase canonical name. */
|
||||
export function slhDsaProviderByName(name: string): SLHDSAProvider {
|
||||
switch (name) {
|
||||
case 'slh-dsa-sha2-128f':
|
||||
return slhDsa128fProvider
|
||||
case 'slh-dsa-sha2-192f':
|
||||
return slhDsa192fProvider
|
||||
case 'slh-dsa-sha2-192s':
|
||||
return slhDsa192sProvider
|
||||
case 'slh-dsa-sha2-256f':
|
||||
return slhDsa256fProvider
|
||||
default:
|
||||
throw new Error(`pq/slhdsa: unknown SLH-DSA provider ${name}`)
|
||||
}
|
||||
}
|
||||
Generated
+295
-48
@@ -279,6 +279,18 @@ importers:
|
||||
typescript:
|
||||
specifier: 5.9.3
|
||||
version: 5.9.3
|
||||
vite-plugin-commonjs:
|
||||
specifier: ^0.10.4
|
||||
version: 0.10.4
|
||||
vite-plugin-node-polyfills:
|
||||
specifier: ^0.24.0
|
||||
version: 0.24.0(rollup@4.60.1)(vite@8.0.8(@types/node@22.13.1)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.3))
|
||||
vite-plugin-svgr:
|
||||
specifier: ^4.3.0
|
||||
version: 4.5.0(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@22.13.1)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.3))
|
||||
vite-tsconfig-paths:
|
||||
specifier: ^5.1.4
|
||||
version: 5.1.4(typescript@5.9.3)(vite@8.0.8(@types/node@22.13.1)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.3))
|
||||
webpack:
|
||||
specifier: 5.90.0
|
||||
version: 5.90.0(@swc/core@1.15.24)(esbuild@0.27.7)(webpack-cli@5.1.4)
|
||||
@@ -835,6 +847,9 @@ importers:
|
||||
'@noble/hashes':
|
||||
specifier: ^1.7.1
|
||||
version: 1.8.0
|
||||
'@noble/post-quantum':
|
||||
specifier: 0.6.1
|
||||
version: 0.6.1
|
||||
'@scure/bip32':
|
||||
specifier: ^1.5.0
|
||||
version: 1.7.0
|
||||
@@ -966,6 +981,15 @@ importers:
|
||||
'@luxamm/swap-sdk':
|
||||
specifier: 3.0.1
|
||||
version: 3.0.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)
|
||||
'@noble/curves':
|
||||
specifier: 1.9.7
|
||||
version: 1.9.7
|
||||
'@noble/hashes':
|
||||
specifier: ^1.7.1
|
||||
version: 1.8.0
|
||||
'@noble/post-quantum':
|
||||
specifier: 0.6.1
|
||||
version: 0.6.1
|
||||
'@react-navigation/core':
|
||||
specifier: 7.9.2
|
||||
version: 7.9.2(react@19.2.5)
|
||||
@@ -978,6 +1002,9 @@ importers:
|
||||
'@scure/bip32':
|
||||
specifier: 1.3.2
|
||||
version: 1.3.2
|
||||
'@scure/bip39':
|
||||
specifier: 1.6.0
|
||||
version: 1.6.0
|
||||
'@shopify/react-native-skia':
|
||||
specifier: 2.2.20
|
||||
version: 2.2.20(@hanzogui/react-native-reanimated@3.19.3-fork.1(@babel/core@7.26.0)(@hanzogui/react-native@0.79.5-fork.1)(react@19.2.5))(@hanzogui/react-native@0.79.5-fork.1)(react@19.2.5)
|
||||
@@ -1070,7 +1097,7 @@ importers:
|
||||
version: 1.5.0(redux-saga@1.2.2)
|
||||
viem:
|
||||
specifier: 2.30.5
|
||||
version: 2.30.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
|
||||
version: 2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
zod:
|
||||
specifier: npm:@hanzogui/zod@4.3.6-fork.1
|
||||
version: '@hanzogui/zod@4.3.6-fork.1'
|
||||
@@ -5728,6 +5755,10 @@ packages:
|
||||
resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
|
||||
'@noble/ciphers@2.2.0':
|
||||
resolution: {integrity: sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
'@noble/curves@1.2.0':
|
||||
resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==}
|
||||
|
||||
@@ -5754,6 +5785,10 @@ packages:
|
||||
resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
|
||||
'@noble/curves@2.2.0':
|
||||
resolution: {integrity: sha512-T/BoHgFXirb0ENSPBquzX0rcjXeM6Lo892a2jlYJkqk83LqZx0l1Of7DzlKJ6jkpvMrkHSnAcgb5JegL8SeIkQ==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
'@noble/hashes@1.2.0':
|
||||
resolution: {integrity: sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==}
|
||||
|
||||
@@ -5789,6 +5824,14 @@ packages:
|
||||
resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
'@noble/hashes@2.2.0':
|
||||
resolution: {integrity: sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
'@noble/post-quantum@0.6.1':
|
||||
resolution: {integrity: sha512-+pormrDZwjRw05U8ADK4JpHejo87+gBd+muRBB/ozztH5yhDLMDF4jHQWN3NQQAsu1zBNPWTG0ZwVI0CR29H0A==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
'@noble/secp256k1@1.7.1':
|
||||
resolution: {integrity: sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==}
|
||||
|
||||
@@ -6218,6 +6261,7 @@ packages:
|
||||
|
||||
'@react-navigation/bottom-tabs@6.6.1':
|
||||
resolution: {integrity: sha512-9oD4cypEBjPuaMiu9tevWGiQ4w/d6l3HNhcJ1IjXZ24xvYDSs0mqjUcdt8SWUolCvRrYc/DmNBLlT83bk0bHTw==}
|
||||
deprecated: This version is no longer supported
|
||||
peerDependencies:
|
||||
'@react-navigation/native': ^6.0.0
|
||||
react: '*'
|
||||
@@ -6232,6 +6276,7 @@ packages:
|
||||
|
||||
'@react-navigation/elements@1.3.31':
|
||||
resolution: {integrity: sha512-bUzP4Awlljx5RKEExw8WYtif8EuQni2glDaieYROKTnaxsu9kEIA515sXQgUDZU4Ob12VoL7+z70uO3qrlfXcQ==}
|
||||
deprecated: This version is no longer supported
|
||||
peerDependencies:
|
||||
'@react-navigation/native': ^6.0.0
|
||||
react: '*'
|
||||
@@ -6434,6 +6479,24 @@ packages:
|
||||
'@rolldown/pluginutils@1.0.0-rc.15':
|
||||
resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==}
|
||||
|
||||
'@rollup/plugin-inject@5.0.5':
|
||||
resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
|
||||
'@rollup/pluginutils@5.3.0':
|
||||
resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
|
||||
peerDependenciesMeta:
|
||||
rollup:
|
||||
optional: true
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.60.1':
|
||||
resolution: {integrity: sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==}
|
||||
cpu: [arm]
|
||||
@@ -6571,6 +6634,7 @@ packages:
|
||||
'@safe-global/safe-gateway-typescript-sdk@3.23.1':
|
||||
resolution: {integrity: sha512-6ORQfwtEJYpalCeVO21L4XXGSdbEMfyp2hEv6cP82afKXSwvse6d3sdelgaPWUxHIsFRkWvHDdzh8IyyKHZKxw==}
|
||||
engines: {node: '>=16'}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
|
||||
'@scure/base@1.1.9':
|
||||
resolution: {integrity: sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==}
|
||||
@@ -7645,6 +7709,7 @@ packages:
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
|
||||
|
||||
'@uniswap/analytics-events@2.43.0':
|
||||
resolution: {integrity: sha512-NH/Baq7dqnNkBjM+EzylGSuI+Uz7vgvwNmVnYTu8IYKTHe4jFVP570xkcHzgNRJRvMrpoAv1r8dGcTtPn3kmpw==}
|
||||
@@ -8053,6 +8118,7 @@ packages:
|
||||
'@xmldom/xmldom@0.8.12':
|
||||
resolution: {integrity: sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
deprecated: this version has critical issues, please update to the latest version
|
||||
|
||||
'@xtuc/ieee754@1.2.0':
|
||||
resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==}
|
||||
@@ -8737,6 +8803,9 @@ packages:
|
||||
browser-assert@1.2.1:
|
||||
resolution: {integrity: sha512-nfulgvOR6S4gt9UKCeGJOuSGBPGiFT6oQ/2UBnvTY/5aQ1PnksW72fhZkM30DzoRRv2WpwZf1vHHEr3mtuXIWQ==}
|
||||
|
||||
browser-resolve@2.0.0:
|
||||
resolution: {integrity: sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==}
|
||||
|
||||
browser-stdout@1.3.1:
|
||||
resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==}
|
||||
|
||||
@@ -9803,6 +9872,10 @@ packages:
|
||||
dom-walk@0.1.2:
|
||||
resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==}
|
||||
|
||||
domain-browser@4.22.0:
|
||||
resolution: {integrity: sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
domain-browser@4.23.0:
|
||||
resolution: {integrity: sha512-ArzcM/II1wCCujdCNyQjXrAFwS4mrLh4C7DZWlaI8mdh7h3BfKdNd3bKXITfl2PT9FtfQqaGvhi1vPRQPimjGA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -11112,6 +11185,9 @@ packages:
|
||||
resolution: {integrity: sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
globrex@0.1.2:
|
||||
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
||||
|
||||
gonzales-pe@4.3.0:
|
||||
resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==}
|
||||
engines: {node: '>=0.6.0'}
|
||||
@@ -11686,6 +11762,7 @@ packages:
|
||||
|
||||
is-relative-path@1.0.2:
|
||||
resolution: {integrity: sha512-i1h+y50g+0hRbBD+dbnInl3JlJ702aar58snAeX+MxBAPvzXGej7sYoPMhlnykabt0ZzCJNBEyzMlekuQZN7fA==}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
|
||||
is-relative@0.1.3:
|
||||
resolution: {integrity: sha512-wBOr+rNM4gkAZqoLRJI4myw5WzzIdQosFAAbnvfXP5z1LyzgAI3ivOKehC5KfqlQJZoihVhirgtCBj378Eg8GA==}
|
||||
@@ -11778,6 +11855,10 @@ packages:
|
||||
resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
isomorphic-timers-promises@1.0.1:
|
||||
resolution: {integrity: sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
isomorphic-ws@4.0.1:
|
||||
resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==}
|
||||
peerDependencies:
|
||||
@@ -13002,6 +13083,10 @@ packages:
|
||||
resolution: {integrity: sha512-Y4jr/8SRS5hzEdZ7SGuvZGwfORvNsSsNRwDXx5WisiqzsVfeftDvRgfeqWNgZvWSJbgubTRVRYBzK6UO+ErqjA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
node-stdlib-browser@1.3.1:
|
||||
resolution: {integrity: sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
node-stream-zip@1.15.0:
|
||||
resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==}
|
||||
engines: {node: '>=0.12.0'}
|
||||
@@ -13469,6 +13554,10 @@ packages:
|
||||
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
pkg-dir@5.0.0:
|
||||
resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
pkg-types@1.3.1:
|
||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||
|
||||
@@ -14290,6 +14379,7 @@ packages:
|
||||
react-server-dom-webpack@19.0.5:
|
||||
resolution: {integrity: sha512-oPXezLBHTczshAyDIOzwd4etNwDnsPrFZ+es22OS0DOsJPgtcN1X7ukajkk4HChrWCd+jKG7Y5o8iVXYgBcM6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
deprecated: High Security Vulnerability in React Server Components
|
||||
peerDependencies:
|
||||
react: ^19.0.5
|
||||
react-dom: ^19.0.5
|
||||
@@ -15549,6 +15639,16 @@ packages:
|
||||
ts-object-utils@0.0.5:
|
||||
resolution: {integrity: sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==}
|
||||
|
||||
tsconfck@3.1.6:
|
||||
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
tsconfig-paths@3.15.0:
|
||||
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
|
||||
|
||||
@@ -15957,10 +16057,12 @@ packages:
|
||||
|
||||
uuid@9.0.0:
|
||||
resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
uuid@9.0.1:
|
||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
||||
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
|
||||
hasBin: true
|
||||
|
||||
v8-compile-cache-lib@3.0.1:
|
||||
@@ -16011,6 +16113,30 @@ packages:
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
hasBin: true
|
||||
|
||||
vite-plugin-commonjs@0.10.4:
|
||||
resolution: {integrity: sha512-eWQuvQKCcx0QYB5e5xfxBNjQKyrjEWZIR9UOkOV6JAgxVhtbZvCOF+FNC2ZijBJ3U3Px04ZMMyyMyFBVWIJ5+g==}
|
||||
|
||||
vite-plugin-dynamic-import@1.6.0:
|
||||
resolution: {integrity: sha512-TM0sz70wfzTIo9YCxVFwS8OA9lNREsh+0vMHGSkWDTZ7bgd1Yjs5RV8EgB634l/91IsXJReg0xtmuQqP0mf+rg==}
|
||||
|
||||
vite-plugin-node-polyfills@0.24.0:
|
||||
resolution: {integrity: sha512-GA9QKLH+vIM8NPaGA+o2t8PDfFUl32J8rUp1zQfMKVJQiNkOX4unE51tR6ppl6iKw5yOrDAdSH7r/UIFLCVhLw==}
|
||||
peerDependencies:
|
||||
vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
|
||||
vite-plugin-svgr@4.5.0:
|
||||
resolution: {integrity: sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==}
|
||||
peerDependencies:
|
||||
vite: '>=2.6.0'
|
||||
|
||||
vite-tsconfig-paths@5.1.4:
|
||||
resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==}
|
||||
peerDependencies:
|
||||
vite: '*'
|
||||
peerDependenciesMeta:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
vite@7.3.2:
|
||||
resolution: {integrity: sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -33169,7 +33295,7 @@ snapshots:
|
||||
tiny-invariant: 1.3.1
|
||||
typed-redux-saga: 1.5.0(redux-saga@1.2.2)
|
||||
uuid: 9.0.0
|
||||
viem: 2.30.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
|
||||
viem: 2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
wagmi: 2.15.5(@hanzogui/zod@4.3.6-fork.1)(@react-native-async-storage/async-storage@2.1.2(@hanzogui/react-native@0.79.5-fork.1))(@tanstack/react-query@5.90.20(react@19.2.5))(@types/react@19.0.10)(bufferutil@4.1.0)(immer@9.0.21)(react@19.2.5)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))
|
||||
zod: '@hanzogui/zod@4.3.6-fork.1'
|
||||
zustand: 5.0.6(@types/react@19.0.10)(immer@9.0.21)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))
|
||||
@@ -33335,7 +33461,7 @@ snapshots:
|
||||
tiny-invariant: 1.3.1
|
||||
typed-redux-saga: 1.5.0(redux-saga@1.2.2)
|
||||
uuid: 9.0.0
|
||||
viem: 2.30.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
|
||||
viem: 2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
wagmi: 2.15.5(@hanzogui/zod@4.3.6-fork.1)(@react-native-async-storage/async-storage@2.1.2(@hanzogui/react-native@0.79.5-fork.1))(@tanstack/react-query@5.90.20(react@19.2.5))(@types/react@19.0.10)(bufferutil@4.1.0)(immer@9.0.21)(react@19.2.5)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))
|
||||
zod: '@hanzogui/zod@4.3.6-fork.1'
|
||||
zustand: 5.0.6(@types/react@19.0.10)(immer@9.0.21)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))
|
||||
@@ -33774,9 +33900,9 @@ snapshots:
|
||||
'@hanzogui/web': 4.4.0(45f06717636f94e91b4f67f28d45f016)
|
||||
'@l.x/utils': 1.1.3(@babel/core@7.26.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(expo@53.0.22(@babel/core@7.26.0)(bufferutil@4.1.0)(graphql@16.6.0)(react-native-webview@13.13.5(react-native@0.79.5(@babel/core@7.26.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10))(react@19.2.5))(react-native@0.79.5(@babel/core@7.26.0)(@react-native-community/cli@18.0.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@5.0.10))(react@19.2.5)(utf-8-validate@5.0.10))(graphql-ws@5.12.1(graphql@16.6.0))(react-dom@19.2.5(react@19.2.5))(utf-8-validate@5.0.10)
|
||||
'@react-native-masked-view/masked-view': 0.3.2(@hanzogui/react-native@0.79.5-fork.1)(react@19.2.5)
|
||||
'@shopify/flash-list': 1.7.6(@babel/runtime@7.28.2)(@hanzogui/react-native@0.79.5-fork.1)(react@19.2.5)
|
||||
'@shopify/flash-list': 1.7.6(@babel/runtime@7.26.0)(@hanzogui/react-native@0.79.5-fork.1)(react@19.2.5)
|
||||
'@shopify/react-native-skia': 2.2.20(@hanzogui/react-native-reanimated@3.19.3-fork.1(@babel/core@7.26.0)(@hanzogui/react-native@0.79.5-fork.1)(react@19.2.5))(@hanzogui/react-native@0.79.5-fork.1)(react@19.2.5)
|
||||
'@storybook/react': 8.5.2(@storybook/test@8.5.2(storybook@8.6.18(bufferutil@4.1.0)(prettier@3.8.2)(utf-8-validate@5.0.10)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@8.6.18(bufferutil@4.1.0)(prettier@3.8.2)(utf-8-validate@5.0.10))(typescript@5.9.3)
|
||||
'@storybook/react': 8.5.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@8.6.18(bufferutil@4.1.0)(prettier@2.8.8)(utf-8-validate@5.0.10))(typescript@5.9.3)
|
||||
'@tanstack/react-query': 5.90.20(react@19.2.5)
|
||||
'@testing-library/react': 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.6(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
ethers: 5.7.2(bufferutil@4.1.0)(utf-8-validate@5.0.10)
|
||||
@@ -34164,7 +34290,7 @@ snapshots:
|
||||
eslint-plugin-check-file: 2.8.0(eslint@8.57.1)
|
||||
eslint-plugin-detox: 1.0.0
|
||||
eslint-plugin-ft-flow: 2.0.3(@babel/eslint-parser@7.28.6(@babel/core@7.26.0)(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.20.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@6.20.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.20.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
||||
eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@6.20.0(@typescript-eslint/parser@6.20.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(jest@29.7.0(@types/node@22.13.1)(babel-plugin-macros@3.1.0)(node-notifier@10.0.1)(ts-node@10.9.2(@swc/core@1.15.24)(@types/node@22.13.1)(typescript@5.9.3)))(typescript@5.9.3)
|
||||
eslint-plugin-local-rules: 3.0.2
|
||||
eslint-plugin-no-relative-import-paths: 1.5.2
|
||||
@@ -34418,6 +34544,8 @@ snapshots:
|
||||
|
||||
'@noble/ciphers@1.3.0': {}
|
||||
|
||||
'@noble/ciphers@2.2.0': {}
|
||||
|
||||
'@noble/curves@1.2.0':
|
||||
dependencies:
|
||||
'@noble/hashes': 1.3.2
|
||||
@@ -34446,6 +34574,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@noble/hashes': 1.8.0
|
||||
|
||||
'@noble/curves@2.2.0':
|
||||
dependencies:
|
||||
'@noble/hashes': 2.2.0
|
||||
|
||||
'@noble/hashes@1.2.0': {}
|
||||
|
||||
'@noble/hashes@1.3.2': {}
|
||||
@@ -34464,6 +34596,14 @@ snapshots:
|
||||
|
||||
'@noble/hashes@2.0.1': {}
|
||||
|
||||
'@noble/hashes@2.2.0': {}
|
||||
|
||||
'@noble/post-quantum@0.6.1':
|
||||
dependencies:
|
||||
'@noble/ciphers': 2.2.0
|
||||
'@noble/curves': 2.2.0
|
||||
'@noble/hashes': 2.2.0
|
||||
|
||||
'@noble/secp256k1@1.7.1': {}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
@@ -35330,7 +35470,7 @@ snapshots:
|
||||
dependencies:
|
||||
big.js: 6.2.2
|
||||
dayjs: 1.11.13
|
||||
viem: 2.30.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
|
||||
viem: 2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- typescript
|
||||
@@ -35354,7 +35494,7 @@ snapshots:
|
||||
'@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
'@walletconnect/universal-provider': 2.21.0(@hanzogui/zod@4.3.6-fork.1)(@react-native-async-storage/async-storage@2.1.2(@hanzogui/react-native@0.79.5-fork.1))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
valtio: 1.13.2(@types/react@19.0.10)(react@19.2.5)
|
||||
viem: 2.30.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
|
||||
viem: 2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
transitivePeerDependencies:
|
||||
- '@azure/app-configuration'
|
||||
- '@azure/cosmos'
|
||||
@@ -35647,7 +35787,7 @@ snapshots:
|
||||
'@walletconnect/logger': 2.1.2
|
||||
'@walletconnect/universal-provider': 2.21.0(@hanzogui/zod@4.3.6-fork.1)(@react-native-async-storage/async-storage@2.1.2(@hanzogui/react-native@0.79.5-fork.1))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
valtio: 1.13.2(@types/react@19.0.10)(react@19.2.5)
|
||||
viem: 2.30.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
|
||||
viem: 2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
transitivePeerDependencies:
|
||||
- '@azure/app-configuration'
|
||||
- '@azure/cosmos'
|
||||
@@ -35739,7 +35879,7 @@ snapshots:
|
||||
'@walletconnect/universal-provider': 2.21.0(@hanzogui/zod@4.3.6-fork.1)(@react-native-async-storage/async-storage@2.1.2(@hanzogui/react-native@0.79.5-fork.1))(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
bs58: 6.0.0
|
||||
valtio: 1.13.2(@types/react@19.0.10)(react@19.2.5)
|
||||
viem: 2.30.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
|
||||
viem: 2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
transitivePeerDependencies:
|
||||
- '@azure/app-configuration'
|
||||
- '@azure/cosmos'
|
||||
@@ -35898,6 +36038,22 @@ snapshots:
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-rc.15': {}
|
||||
|
||||
'@rollup/plugin-inject@5.0.5(rollup@4.60.1)':
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
rollup: 4.60.1
|
||||
|
||||
'@rollup/pluginutils@5.3.0(rollup@4.60.1)':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
estree-walker: 2.0.2
|
||||
picomatch: 4.0.4
|
||||
optionalDependencies:
|
||||
rollup: 4.60.1
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.60.1':
|
||||
optional: true
|
||||
|
||||
@@ -35998,7 +36154,7 @@ snapshots:
|
||||
'@safe-global/safe-apps-sdk@9.1.0(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)':
|
||||
dependencies:
|
||||
'@safe-global/safe-gateway-typescript-sdk': 3.23.1
|
||||
viem: 2.30.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
|
||||
viem: 2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- typescript
|
||||
@@ -37746,7 +37902,7 @@ snapshots:
|
||||
'@wagmi/core': 2.17.2(@types/react@19.0.10)(immer@9.0.21)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))
|
||||
'@walletconnect/ethereum-provider': 2.21.1(@hanzogui/zod@4.3.6-fork.1)(@react-native-async-storage/async-storage@2.1.2(@hanzogui/react-native@0.79.5-fork.1))(@types/react@19.0.10)(bufferutil@4.1.0)(react@19.2.5)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
cbw-sdk: '@coinbase/wallet-sdk@3.9.3'
|
||||
viem: 2.30.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
|
||||
viem: 2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -37836,7 +37992,7 @@ snapshots:
|
||||
dependencies:
|
||||
eventemitter3: 5.0.1
|
||||
mipd: 0.0.7(typescript@5.9.3)
|
||||
viem: 2.30.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
|
||||
viem: 2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
zustand: 5.0.0(@types/react@19.0.10)(immer@9.0.21)(react@19.2.5)(use-sync-external-store@1.4.0(react@19.2.5))
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
@@ -40123,6 +40279,10 @@ snapshots:
|
||||
|
||||
browser-assert@1.2.1: {}
|
||||
|
||||
browser-resolve@2.0.0:
|
||||
dependencies:
|
||||
resolve: 1.22.12
|
||||
|
||||
browser-stdout@1.3.1: {}
|
||||
|
||||
browserify-aes@1.2.0:
|
||||
@@ -41276,6 +41436,8 @@ snapshots:
|
||||
|
||||
dom-walk@0.1.2: {}
|
||||
|
||||
domain-browser@4.22.0: {}
|
||||
|
||||
domain-browser@4.23.0: {}
|
||||
|
||||
domelementtype@2.3.0: {}
|
||||
@@ -41762,7 +41924,7 @@ snapshots:
|
||||
is-bun-module: 1.3.0
|
||||
is-glob: 4.0.3
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.20.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@6.20.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
|
||||
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@6.20.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
|
||||
transitivePeerDependencies:
|
||||
- '@typescript-eslint/parser'
|
||||
- eslint-import-resolver-node
|
||||
@@ -41803,7 +41965,7 @@ snapshots:
|
||||
lodash: 4.17.21
|
||||
string-natural-compare: 3.0.1
|
||||
|
||||
eslint-plugin-import@2.27.5(@typescript-eslint/parser@6.20.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.20.0(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.27.5(@typescript-eslint/parser@6.20.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
|
||||
eslint-plugin-import@2.27.5(@typescript-eslint/parser@6.20.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1):
|
||||
dependencies:
|
||||
array-includes: 3.1.9
|
||||
array.prototype.flat: 1.3.3
|
||||
@@ -43103,6 +43265,8 @@ snapshots:
|
||||
pify: 2.3.0
|
||||
pinkie-promise: 2.0.1
|
||||
|
||||
globrex@0.1.2: {}
|
||||
|
||||
gonzales-pe@4.3.0:
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
@@ -43756,6 +43920,8 @@ snapshots:
|
||||
|
||||
isobject@3.0.1: {}
|
||||
|
||||
isomorphic-timers-promises@1.0.1: {}
|
||||
|
||||
isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10)):
|
||||
dependencies:
|
||||
ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10)
|
||||
@@ -45338,6 +45504,36 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/parser': 7.29.2
|
||||
|
||||
node-stdlib-browser@1.3.1:
|
||||
dependencies:
|
||||
assert: 2.1.0
|
||||
browser-resolve: 2.0.0
|
||||
browserify-zlib: 0.2.0
|
||||
buffer: 5.7.1
|
||||
console-browserify: 1.2.0
|
||||
constants-browserify: 1.0.0
|
||||
create-require: 1.1.1
|
||||
crypto-browserify: 3.12.1
|
||||
domain-browser: 4.22.0
|
||||
events: 3.3.0
|
||||
https-browserify: 1.0.0
|
||||
isomorphic-timers-promises: 1.0.1
|
||||
os-browserify: 0.3.0
|
||||
path-browserify: 1.0.1
|
||||
pkg-dir: 5.0.0
|
||||
process: 0.11.10
|
||||
punycode: 1.4.1
|
||||
querystring-es3: 0.2.1
|
||||
readable-stream: 3.6.2
|
||||
stream-browserify: 3.0.0
|
||||
stream-http: 3.2.0
|
||||
string_decoder: 1.3.0
|
||||
timers-browserify: 2.0.12
|
||||
tty-browserify: 0.0.1
|
||||
url: 0.11.4
|
||||
util: 0.12.5
|
||||
vm-browserify: 1.1.2
|
||||
|
||||
node-stream-zip@1.15.0: {}
|
||||
|
||||
node-vibrant@3.1.6:
|
||||
@@ -45606,6 +45802,21 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
ox@0.7.1(@hanzogui/zod@4.3.6-fork.1)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@adraffy/ens-normalize': 1.11.1
|
||||
'@noble/ciphers': 1.3.0
|
||||
'@noble/curves': 1.9.7
|
||||
'@noble/hashes': 1.8.0
|
||||
'@scure/bip32': 1.7.0
|
||||
'@scure/bip39': 1.6.0
|
||||
abitype: 1.2.3(@hanzogui/zod@4.3.6-fork.1)(typescript@5.9.3)
|
||||
eventemitter3: 5.0.1
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
ox@0.7.1(typescript@5.9.3)(zod@3.22.4):
|
||||
dependencies:
|
||||
'@adraffy/ens-normalize': 1.11.1
|
||||
@@ -45621,21 +45832,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
ox@0.7.1(typescript@5.9.3)(zod@4.3.6):
|
||||
dependencies:
|
||||
'@adraffy/ens-normalize': 1.11.1
|
||||
'@noble/ciphers': 1.3.0
|
||||
'@noble/curves': 1.9.7
|
||||
'@noble/hashes': 1.8.0
|
||||
'@scure/bip32': 1.7.0
|
||||
'@scure/bip39': 1.6.0
|
||||
abitype: 1.2.3(@hanzogui/zod@4.3.6-fork.1)(typescript@5.9.3)
|
||||
eventemitter3: 5.0.1
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- zod
|
||||
|
||||
ox@0.9.3(typescript@5.9.3)(zod@3.22.4):
|
||||
dependencies:
|
||||
'@adraffy/ens-normalize': 1.11.1
|
||||
@@ -45919,6 +46115,10 @@ snapshots:
|
||||
dependencies:
|
||||
find-up: 4.1.0
|
||||
|
||||
pkg-dir@5.0.0:
|
||||
dependencies:
|
||||
find-up: 5.0.0
|
||||
|
||||
pkg-types@1.3.1:
|
||||
dependencies:
|
||||
confbox: 0.1.8
|
||||
@@ -48538,6 +48738,10 @@ snapshots:
|
||||
|
||||
ts-object-utils@0.0.5: {}
|
||||
|
||||
tsconfck@3.1.6(typescript@5.9.3):
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
tsconfig-paths@3.15.0:
|
||||
dependencies:
|
||||
'@types/json5': 0.0.29
|
||||
@@ -48979,6 +49183,23 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- zod
|
||||
|
||||
viem@2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10):
|
||||
dependencies:
|
||||
'@noble/curves': 1.9.1
|
||||
'@noble/hashes': 1.8.0
|
||||
'@scure/bip32': 1.7.0
|
||||
'@scure/bip39': 1.6.0
|
||||
abitype: 1.0.8(@hanzogui/zod@4.3.6-fork.1)(typescript@5.9.3)
|
||||
isows: 1.0.7(ws@8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10))
|
||||
ox: 0.7.1(@hanzogui/zod@4.3.6-fork.1)(typescript@5.9.3)
|
||||
ws: 8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
- zod
|
||||
|
||||
viem@2.30.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4):
|
||||
dependencies:
|
||||
'@noble/curves': 1.9.1
|
||||
@@ -48996,23 +49217,6 @@ snapshots:
|
||||
- utf-8-validate
|
||||
- zod
|
||||
|
||||
viem@2.30.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6):
|
||||
dependencies:
|
||||
'@noble/curves': 1.9.1
|
||||
'@noble/hashes': 1.8.0
|
||||
'@scure/bip32': 1.7.0
|
||||
'@scure/bip39': 1.6.0
|
||||
abitype: 1.0.8(@hanzogui/zod@4.3.6-fork.1)(typescript@5.9.3)
|
||||
isows: 1.0.7(ws@8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10))
|
||||
ox: 0.7.1(typescript@5.9.3)(zod@4.3.6)
|
||||
ws: 8.18.2(bufferutil@4.1.0)(utf-8-validate@5.0.10)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
- zod
|
||||
|
||||
vite-node@3.2.4(@types/node@22.13.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
@@ -49034,6 +49238,49 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-plugin-commonjs@0.10.4:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
magic-string: 0.30.21
|
||||
vite-plugin-dynamic-import: 1.6.0
|
||||
|
||||
vite-plugin-dynamic-import@1.6.0:
|
||||
dependencies:
|
||||
acorn: 8.16.0
|
||||
es-module-lexer: 1.7.0
|
||||
fast-glob: 3.3.3
|
||||
magic-string: 0.30.21
|
||||
|
||||
vite-plugin-node-polyfills@0.24.0(rollup@4.60.1)(vite@8.0.8(@types/node@22.13.1)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@rollup/plugin-inject': 5.0.5(rollup@4.60.1)
|
||||
node-stdlib-browser: 1.3.1
|
||||
vite: 8.0.8(@types/node@22.13.1)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
|
||||
vite-plugin-svgr@4.5.0(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@22.13.1)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@svgr/core': 8.1.0(typescript@5.9.3)
|
||||
'@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))
|
||||
vite: 8.0.8(@types/node@22.13.1)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@8.0.8(@types/node@22.13.1)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@8.1.1)
|
||||
globrex: 0.1.2
|
||||
tsconfck: 3.1.6(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
vite: 8.0.8(@types/node@22.13.1)(esbuild@0.27.7)(jiti@2.6.1)(terser@5.46.1)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
vite@7.3.2(@types/node@22.13.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(yaml@2.8.3):
|
||||
dependencies:
|
||||
esbuild: 0.27.7
|
||||
@@ -49086,7 +49333,7 @@ snapshots:
|
||||
'@wagmi/core': 2.17.2(@types/react@19.0.10)(immer@9.0.21)(react@19.2.5)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.5))(viem@2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))
|
||||
react: 19.2.5
|
||||
use-sync-external-store: 1.4.0(react@19.2.5)
|
||||
viem: 2.30.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@4.3.6)
|
||||
viem: 2.30.5(@hanzogui/zod@4.3.6-fork.1)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
|
||||
Reference in New Issue
Block a user