diff --git a/packages/keyring/src/keyring.ts b/packages/keyring/src/keyring.ts index 36eb7ab83..a1d8145d0 100644 --- a/packages/keyring/src/keyring.ts +++ b/packages/keyring/src/keyring.ts @@ -5,7 +5,7 @@ import type { EncryptedJsonEncoding, Keypair, KeypairType } from '@polkadot/util import type { KeyringInstance, KeyringOptions, KeyringPair, KeyringPair$Json, KeyringPair$Meta } from './types.js'; import { hexToU8a, isHex, stringToU8a } from '@polkadot/util'; -import { base64Decode, decodeAddress, ed25519PairFromSeed as ed25519FromSeed, encodeAddress, ethereumEncode, hdEthereum, keyExtractSuri, keyFromPath, mnemonicToLegacySeed, mnemonicToMiniSecret, secp256k1PairFromSeed as secp256k1FromSeed, sr25519PairFromSeed as sr25519FromSeed } from '@polkadot/util-crypto'; +import { assertSubstrateType, base64Decode, decodeAddress, ed25519PairFromSeed as ed25519FromSeed, encodeAddress, ethereumEncode, hdEthereum, keyExtractSuri, keyFromPath, mnemonicToLegacySeed, mnemonicToMiniSecret, secp256k1PairFromSeed as secp256k1FromSeed, sr25519PairFromSeed as sr25519FromSeed } from '@polkadot/util-crypto'; import { createPair } from './pair/index.js'; import { DEV_PHRASE } from './defaults.js'; @@ -144,7 +144,7 @@ export class Keyring implements KeyringInstance { */ public addFromSeed (seed: Uint8Array, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair { return this.addPair( - createPair({ toSS58: this.encodeAddress, type }, PairFromSeed[type](seed), meta, null) + createPair({ toSS58: this.encodeAddress, type }, PairFromSeed[assertSubstrateType(type, 'Deriving a pair from a seed')](seed), meta, null) ); } @@ -232,9 +232,9 @@ export class Keyring implements KeyringInstance { const derived = type === 'ethereum' ? isPhraseHex - ? PairFromSeed[type](seed) // for eth, if the private key is provided as suri, it must be derived only once + ? PairFromSeed[assertSubstrateType(type, 'Deriving a pair from a seed')](seed) // for eth, if the private key is provided as suri, it must be derived only once : hdEthereum(seed, derivePath.substring(1)) - : keyFromPath(PairFromSeed[type](seed), path, type); + : keyFromPath(PairFromSeed[assertSubstrateType(type, 'Deriving a pair from a seed')](seed), path, type); return createPair({ toSS58: this.encodeAddress, type }, derived, meta, null); } diff --git a/packages/keyring/src/pair/index.ts b/packages/keyring/src/pair/index.ts index 715722e04..46592f48b 100644 --- a/packages/keyring/src/pair/index.ts +++ b/packages/keyring/src/pair/index.ts @@ -6,7 +6,7 @@ import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta, SignOptions } fro import type { PairInfo } from './types.js'; import { objectSpread, u8aConcat, u8aEmpty, u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util'; -import { blake2AsU8a, ed25519PairFromSeed as ed25519FromSeed, ed25519Sign, ethereumEncode, keccakAsU8a, keyExtractPath, keyFromPath, secp256k1Compress, secp256k1Expand, secp256k1PairFromSeed as secp256k1FromSeed, secp256k1Sign, signatureVerify, sr25519PairFromSeed as sr25519FromSeed, sr25519Sign, sr25519VrfSign, sr25519VrfVerify } from '@polkadot/util-crypto'; +import { assertSubstrateType, blake2AsU8a, ed25519PairFromSeed as ed25519FromSeed, ed25519Sign, ethereumEncode, keccakAsU8a, keyExtractPath, keyFromPath, secp256k1Compress, secp256k1Expand, secp256k1PairFromSeed as secp256k1FromSeed, secp256k1Sign, signatureVerify, sr25519PairFromSeed as sr25519FromSeed, sr25519Sign, sr25519VrfSign, sr25519VrfVerify } from '@polkadot/util-crypto'; import { decodePair } from './decode.js'; import { encodePair } from './encode.js'; @@ -94,7 +94,7 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P publicKey = decoded.publicKey; secretKey = decoded.secretKey; } else { - const pair = TYPE_FROM_SEED[type](decoded.secretKey); + const pair = TYPE_FROM_SEED[assertSubstrateType(type, 'Deriving a pair from a seed')](decoded.secretKey); publicKey = pair.publicKey; secretKey = pair.secretKey; @@ -111,7 +111,7 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P }; const encodeAddress = (): string => { - const raw = TYPE_ADDRESS[type](publicKey); + const raw = TYPE_ADDRESS[assertSubstrateType(type, 'Deriving an address')](publicKey); return type === 'ethereum' ? ethereumEncode(raw) @@ -123,7 +123,7 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P return encodeAddress(); }, get addressRaw (): Uint8Array { - const raw = TYPE_ADDRESS[type](publicKey); + const raw = TYPE_ADDRESS[assertSubstrateType(type, 'Deriving an address')](publicKey); return type === 'ethereum' ? raw.slice(-20) @@ -171,9 +171,9 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P return u8aConcat( options.withType - ? TYPE_PREFIX[type] + ? TYPE_PREFIX[assertSubstrateType(type, 'Signing')] : SIG_TYPE_NONE, - TYPE_SIGNATURE[type](u8aToU8a(message), { publicKey, secretKey }) + TYPE_SIGNATURE[assertSubstrateType(type, 'Signing')](u8aToU8a(message), { publicKey, secretKey }) ); }, toJson: (passphrase?: string): KeyringPair$Json => { @@ -192,7 +192,7 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P return decodePkcs8(passphrase); }, verify: (message: string | Uint8Array, signature: string | Uint8Array, signerPublic: string | Uint8Array): boolean => { - return signatureVerify(message, signature, TYPE_ADDRESS[type](u8aToU8a(signerPublic))).isValid; + return signatureVerify(message, signature, TYPE_ADDRESS[assertSubstrateType(type, 'Verifying')](u8aToU8a(signerPublic))).isValid; }, vrfSign: (message: string | Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): Uint8Array => { if (isLocked(secretKey)) { @@ -203,7 +203,7 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P return sr25519VrfSign(message, { secretKey }, context, extra); } - const proof = TYPE_SIGNATURE[type](u8aToU8a(message), { publicKey, secretKey }); + const proof = TYPE_SIGNATURE[assertSubstrateType(type, 'Signing')](u8aToU8a(message), { publicKey, secretKey }); return u8aConcat(vrfHash(proof, context, extra), proof); }, @@ -212,7 +212,7 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P return sr25519VrfVerify(message, vrfResult, publicKey, context, extra); } - const result = signatureVerify(message, u8aConcat(TYPE_PREFIX[type], vrfResult.subarray(32)), TYPE_ADDRESS[type](u8aToU8a(signerPublic))); + const result = signatureVerify(message, u8aConcat(TYPE_PREFIX[assertSubstrateType(type, 'VRF verification')], vrfResult.subarray(32)), TYPE_ADDRESS[assertSubstrateType(type, 'Verifying')](u8aToU8a(signerPublic))); return result.isValid && u8aEq(vrfResult.subarray(0, 32), vrfHash(vrfResult.subarray(32), context, extra)); } diff --git a/packages/networks/src/defaults.spec.ts b/packages/networks/src/defaults.spec.ts index 7ac07cfd3..4f93560ca 100644 --- a/packages/networks/src/defaults.spec.ts +++ b/packages/networks/src/defaults.spec.ts @@ -22,10 +22,15 @@ describe('defaults', (): void => { ).toEqual([]); }); - it('has no entries for testnets', (): void => { + // Upstream asserted the opposite: that no testnet appears in knownGenesis. + // This fork carries Quantus testnets deliberately — a wallet for a chain + // under development has to reach its testnet — so the assertion becomes that + // any testnet listed here is one we meant to list. See defaults/genesis.ts. + it('has genesis entries only for testnets we deliberately support', (): void => { expect( genesisKeys.filter((network) => - knownTestnet[network] + knownTestnet[network] && + !network.startsWith('quantus') ) ).toEqual([]); }); diff --git a/packages/networks/src/defaults/genesis.ts b/packages/networks/src/defaults/genesis.ts index 27342c2d7..3f5dddad3 100644 --- a/packages/networks/src/defaults/genesis.ts +++ b/packages/networks/src/defaults/genesis.ts @@ -8,9 +8,20 @@ import type { KnownGenesis } from '../types.js'; // oldest goes last. This make lookups for the current a simple genesisHash[0] // where the latest ios always the first entry (See Kusama as an example) // -// IMPORTANT: Apart from the test relays, this list is limited to live parachains -// and live production networks. It does not and should not contain any testnets, -// either stand-alone or connected to test relays such as Westend/Rococo +// UPSTREAM SAID: "Apart from the test relays, this list is limited to live +// parachains and live production networks. It does not and should not contain +// any testnets, either stand-alone or connected to test relays such as +// Westend/Rococo" +// +// THIS FORK DOES NOT. That rule keeps testnets out of polkadot-js/apps' chain +// picker, which is a sensible product decision for a production explorer and the +// wrong one for us. Quantus is a chain under active development, and a wallet +// that cannot talk to its testnet is a wallet that cannot be tested against +// anything but real funds. Testnets are first-class here. +// +// `isTestnet` is still set honestly, so a consumer that wants to hide them can; +// what changed is that we no longer make that choice on the consumer's behalf. +// See `toExpanded` in ../interfaces.ts. export const knownGenesis: KnownGenesis = { acala: [ '0xfc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c' @@ -153,6 +164,18 @@ export const knownGenesis: KnownGenesis = { polymesh: [ '0x6fbd74e5e1d0a61d52ccfe9d4adaed16dd3a7caa37c6bc4d0c2fa12e8b2f4063' ], + // Quantus mainnet, read from the node with `chain_getBlockHash(0)` on + // 2026-09-09, the day it launched. Not the staging chain, which is + // `0xb0e90ed9f291a9194f79…` and which telemetry confusingly calls + // "Quantus Staging Mainnet". + quantus: [ + '0xfb5487c0be6ae4ade2d41d16e50465129861636c2b8d61fa94d7a19631626fba' + ], + // Heisenberg, the public Quantus testnet. A testnet carrying a genesis hash is + // a deliberate departure from upstream — see the note above. + 'quantus-heisenberg': [ + '0xa5aa9e5c84d4a3722c152295e7973c9af522f2fb1ef7db5afaa3d5f4dc8d3b4f' + ], quartz: [ '0xcd4d732201ebe5d6b014edda071c4203e16867305332301dc8d092044b28e554' ], diff --git a/packages/networks/src/defaults/index.ts b/packages/networks/src/defaults/index.ts index 4fe229359..7be0555e1 100644 --- a/packages/networks/src/defaults/index.ts +++ b/packages/networks/src/defaults/index.ts @@ -4,4 +4,5 @@ export { knownGenesis } from './genesis.js'; export { knownIcon } from './icons.js'; export { knownLedger } from './ledger.js'; -export { knownTestnet } from './testnets.js'; +export { knownQuantus } from './quantus.js'; +export { exposedTestnet, knownTestnet } from './testnets.js'; diff --git a/packages/networks/src/defaults/quantus.ts b/packages/networks/src/defaults/quantus.ts new file mode 100644 index 000000000..5e58ddcef --- /dev/null +++ b/packages/networks/src/defaults/quantus.ts @@ -0,0 +1,36 @@ +// Copyright 2017-2026 @polkadot/networks authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { KnownSubstrate } from '../types.js'; + +// Quantus networks, which `@substrate/ss58-registry` does not carry. +// +// Prefix 189 is unclaimed in that registry — nothing occupies 185..195 at all — +// so this is an addition rather than an override. It lives in this fork because +// the registry is Parity-curated and post-quantum support has not landed +// upstream; see the standing convention in quantus/extension#1. +// +// `standardAccount: 'ml-dsa'` is a value upstream's type does not allow, which +// is why `KnownSubstrate` is widened in ../types.ts. It is load-bearing: a null +// or missing `standardAccount` makes `toExpanded` mark the network `isIgnored` +// and it disappears from `availableNetworks`. +export const knownQuantus: KnownSubstrate[] = [ + { + decimals: [12], + displayName: 'Quantus', + network: 'quantus', + prefix: 189, + standardAccount: 'ml-dsa', + symbols: ['QTC'], + website: 'https://quantus.com' + }, + { + decimals: [12], + displayName: 'Quantus Heisenberg', + network: 'quantus-heisenberg', + prefix: 189, + standardAccount: 'ml-dsa', + symbols: ['QTC'], + website: 'https://quantus.com' + } +]; diff --git a/packages/networks/src/defaults/testnets.ts b/packages/networks/src/defaults/testnets.ts index 16606563b..b2109edc8 100644 --- a/packages/networks/src/defaults/testnets.ts +++ b/packages/networks/src/defaults/testnets.ts @@ -3,7 +3,8 @@ import type { KnownTestnet } from '../types.js'; -// testnets should not allow selection +// Marks a network as a testnet. Purely descriptive — see `exposedTestnet` below +// for which of them this fork actually offers. export const knownTestnet: KnownTestnet = { '': true, // this is the default non-network entry 'cess-testnet': true, @@ -11,6 +12,24 @@ export const knownTestnet: KnownTestnet = { jupiter: true, 'mathchain-testnet': true, p3dt: true, + 'quantus-heisenberg': true, subspace_testnet: true, 'zero-alphaville': true }; + +// Testnets this fork exposes anyway, despite upstream's rule that a testnet is +// never offered. +// +// That rule keeps testnets out of polkadot-js/apps' chain picker, which is right +// for a production explorer and wrong for us: Quantus is under active +// development, and a wallet that cannot reach its testnet can only be tested +// against real funds. +// +// An explicit allowlist rather than dropping the rule wholesale. Dropping it +// exposed all fifteen of upstream's testnets too, which is a change we have no +// reason to make and no way to have thought about — it moved the tail of the +// sorted list from 'ZERO' to 'ZERO Alphaville' and would have surprised anyone +// reading `availableNetworks`. Opting in one network at a time says what we mean. +export const exposedTestnet: KnownTestnet = { + 'quantus-heisenberg': true +}; diff --git a/packages/networks/src/index.spec.ts b/packages/networks/src/index.spec.ts index 25a527c60..8396fc099 100644 --- a/packages/networks/src/index.spec.ts +++ b/packages/networks/src/index.spec.ts @@ -51,16 +51,29 @@ describe('availableNetworks', (): void => { ).toEqual([]); }); - it('has no testnets exposed', (): void => { + // Upstream asserted that no testnet reaches availableNetworks at all. This + // fork exposes the Quantus ones on purpose — see toExpanded in interfaces.ts — + // so what is worth pinning instead is that an exposed testnet is still + // *labelled* one. Losing the label is the failure that would matter: a user + // cannot tell play money from real money by looking at an address. + it('labels every exposed testnet as a testnet', (): void => { expect( Object.keys(knownTestnet).filter((network) => availableNetworks.some((a) => - a.network === network + a.network === network && !a.isTestnet ) ) ).toEqual([]); }); + it('exposes no testnets except the ones we opted into', (): void => { + expect( + availableNetworks + .filter((a) => a.isTestnet && !a.network.startsWith('quantus')) + .map((a) => a.network) + ).toEqual([]); + }); + it('has allNetworks icons, except for overrides', (): void => { expect( availableNetworks.filter(({ icon, network }) => @@ -92,14 +105,26 @@ describe('availableNetworks', (): void => { }); describe('allNetworks', (): void => { - it('has no ss58 duplicates', (): void => { + // Upstream asserted one network per SS58 prefix. That holds for a registry of + // distinct chains and stops holding the moment a chain has a testnet: Quantus + // mainnet and Heisenberg are both prefix 189, because they are the same chain + // at two deployments and an address is valid on either. Pretending otherwise + // would mean giving one of them a prefix it does not use, and printing + // addresses no Quantus tool would recognise. + // + // Consumers key on genesisHash, not prefix — the extension's chains.ts builds + // its list from `genesisHash[0]` — so sharing a prefix costs nothing there. + // What is still worth pinning is that a duplicate is deliberate. + it('has no ss58 duplicates, except deployments of the same chain', (): void => { const dupes: SubstrateNetwork[] = []; const uniques: SubstrateNetwork[] = []; allNetworks.forEach((a): void => { - if (uniques.some((u) => u.prefix === a.prefix)) { + const clash = uniques.find((u) => u.prefix === a.prefix); + + if (clash && clash.network.split('-')[0] !== a.network.split('-')[0]) { dupes.push(a); - } else { + } else if (!clash) { uniques.push(a); } }); diff --git a/packages/networks/src/interfaces.ts b/packages/networks/src/interfaces.ts index 38ddaad1e..6a0c451f3 100644 --- a/packages/networks/src/interfaces.ts +++ b/packages/networks/src/interfaces.ts @@ -5,7 +5,7 @@ import type { KnownSubstrate, Network, SubstrateNetwork } from './types.js'; import knownSubstrate from '@substrate/ss58-registry'; -import { knownGenesis, knownIcon, knownLedger, knownTestnet } from './defaults/index.js'; +import { exposedTestnet, knownGenesis, knownIcon, knownLedger, knownQuantus, knownTestnet } from './defaults/index.js'; // These are known prefixes that are not sorted const UNSORTED = [0, 2, 42]; @@ -26,7 +26,16 @@ function toExpanded (o: KnownSubstrate): SubstrateNetwork { // filtering n.isTestnet = !!knownTestnet[network] || TESTNETS.includes(nameParts[nameParts.length - 1]); - n.isIgnored = n.isTestnet || ( + + // Upstream had a bare `isTestnet ||` here, so a testnet was dropped no matter + // how complete its entry was. That suits a production chain picker and does + // not suit us — Quantus development happens on Heisenberg, and tools that + // cannot reach a testnet can only be tested against real funds. + // + // The exception is an allowlist rather than a blanket removal: see + // `exposedTestnet`. `isTestnet` is still reported honestly either way, so a + // consumer can label or filter — we simply no longer make that call for them. + n.isIgnored = (n.isTestnet && !exposedTestnet[network]) || ( !( o.standardAccount && o.decimals?.length && @@ -60,7 +69,13 @@ function sortNetworks (a: Network, b: Network): number { } // This is all the Substrate networks with our additional information -export const allNetworks = knownSubstrate.map(toExpanded); +// +// Quantus is appended rather than merged: `@substrate/ss58-registry` does not +// carry it and prefix 189 is unclaimed there, so there is nothing to override. +// Appending also means a registry bump cannot silently drop it. +export const allNetworks = knownSubstrate + .map((o) => toExpanded(o as KnownSubstrate)) + .concat(knownQuantus.map(toExpanded)); // The list of available/claimed prefixes // - no testnets diff --git a/packages/networks/src/types.ts b/packages/networks/src/types.ts index 7750f4cd2..1813c766a 100644 --- a/packages/networks/src/types.ts +++ b/packages/networks/src/types.ts @@ -12,7 +12,19 @@ export type KnownLedger = Record; export type KnownGenesis = Record; -export type KnownSubstrate = RegistryEntry; +/** + * A registry entry, widened for schemes the upstream registry does not know about. + * + * `@substrate/ss58-registry` types `standardAccount` as a closed union of the + * curves Substrate ships with. Quantus signs with ML-DSA, which is not one of + * them and — given the registry is a Parity-curated list and post-quantum + * support has not landed upstream — is not going to become one. Widening here + * keeps the Quantus entry in `defaults/quantus.ts` type-checked rather than + * cast, and leaves every upstream entry exactly as the registry declares it. + */ +export type KnownSubstrate = Omit & { + standardAccount: RegistryEntry['standardAccount'] | 'ml-dsa'; +}; export type KnownTestnet = Record; diff --git a/packages/util-crypto/src/bundle.ts b/packages/util-crypto/src/bundle.ts index 52b3f09c2..7a118ab7d 100644 --- a/packages/util-crypto/src/bundle.ts +++ b/packages/util-crypto/src/bundle.ts @@ -13,6 +13,7 @@ export * from './base58/index.js'; export * from './base64/index.js'; export * from './blake2/index.js'; export * from './crypto.js'; +export * from './dilithium.js'; export * from './ed25519/index.js'; export * from './ethereum/index.js'; export * from './hd/index.js'; diff --git a/packages/util-crypto/src/dilithium.ts b/packages/util-crypto/src/dilithium.ts new file mode 100644 index 000000000..c3a406b0f --- /dev/null +++ b/packages/util-crypto/src/dilithium.ts @@ -0,0 +1,43 @@ +// Copyright 2017-2026 @polkadot/util-crypto authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { KeypairType } from './types.js'; + +/** The Quantus post-quantum keypair types. */ +export const DILITHIUM_TYPES = ['dilithium65', 'dilithium87'] as const; + +/** A Quantus post-quantum keypair type. */ +export type DilithiumType = (typeof DILITHIUM_TYPES)[number]; + +/** The keypair types that predate Quantus, i.e. everything upstream supports. */ +export type SubstrateKeypairType = Exclude; + +/** Whether `type` is one of the Quantus post-quantum schemes. */ +export function isDilithium (type: KeypairType): type is DilithiumType { + return type === 'dilithium65' || type === 'dilithium87'; +} + +/** + * Narrow a `KeypairType` to the ones the curve-based code paths understand, + * throwing for the Quantus schemes. + * + * Opening up `KeypairType` turns every lookup table keyed by it into a type + * error — which is the point, since it enumerates precisely the places that need + * a post-quantum answer. Where that answer is not written yet, this is how the + * gap is expressed: a named, typed refusal at the call site rather than a silent + * `undefined` from an object index. + * + * It throws rather than degrading because every one of these paths produces key + * material or an address. Returning a wrong-but-plausible value would mean funds + * at an address nobody controls, discovered later and unrecoverably. + * + * Call sites disappear as the arms are implemented — see quantus/common#2 for + * the pair maps and quantus/common#4 for derivation. + */ +export function assertSubstrateType (type: KeypairType, what: string): SubstrateKeypairType { + if (isDilithium(type)) { + throw new Error(`${what} is not implemented for ${type}`); + } + + return type; +} diff --git a/packages/util-crypto/src/key/fromPath.ts b/packages/util-crypto/src/key/fromPath.ts index 550446357..9cdcfc123 100644 --- a/packages/util-crypto/src/key/fromPath.ts +++ b/packages/util-crypto/src/key/fromPath.ts @@ -17,7 +17,21 @@ const generators = { }; export function keyFromPath (pair: Keypair, path: DeriveJunction[], type: KeypairType): Keypair { - const keyHdkd = generators[type]; + const keyHdkd = generators[type as keyof typeof generators] as typeof keyHdkdEd25519 | undefined; + + // ML-DSA has no junction derivation to fall back on. Lattice keys are not + // publicly derivable, so a soft junction cannot mean what BIP-32 implies, and + // the chain's own `Pair::derive` refuses for the same reason. Quantus keys + // derive from a hardened BIP44 path instead — see quantus/common#4 — which is + // a different entry point, not a different generator in this map. + // + // This throws rather than returning the pair unchanged: silently ignoring a + // derivation path would hand the caller the *parent* key under a child's + // name, and fund an address nobody asked for. + if (!keyHdkd) { + throw new Error(`Unable to derive on this keypair type: ${type}`); + } + let result = pair; for (const junction of path) { diff --git a/packages/util-crypto/src/types.ts b/packages/util-crypto/src/types.ts index 206922383..3be8b9afa 100644 --- a/packages/util-crypto/src/types.ts +++ b/packages/util-crypto/src/types.ts @@ -18,8 +18,19 @@ export interface Seedpair { seed: Uint8Array; } -/** The supported types of pairs */ -export type KeypairType = 'ed25519' | 'sr25519' | 'ecdsa' | 'ethereum'; +/** + * The supported types of pairs + * + * `dilithium65` and `dilithium87` are the Quantus post-quantum schemes — ML-DSA-65 + * and ML-DSA-87 respectively. They are two entries rather than one `dilithium` + * because the parameter set is not an implementation detail: it selects the + * signature enum variant byte the chain reads off the wire, it selects the + * trailing hardened index of the derivation path, and it fixes the key and + * signature lengths. New Quantus accounts use `dilithium65`; `dilithium87` is + * legacy, used by accounts created before the scheme was recorded and by the + * dev-genesis accounts, so it must be supported but never chosen. + */ +export type KeypairType = 'ed25519' | 'sr25519' | 'ecdsa' | 'ethereum' | 'dilithium65' | 'dilithium87'; export interface VerifyResult { /** The detected crypto interface, or 'none' if not detected */