feat(util-crypto,networks): open KeypairType to ML-DSA, register Quantus

Adds dilithium65 and dilithium87 to KeypairType, and Quantus to the network
registry at SS58 prefix 189.

Two types rather than one 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 accounts use dilithium65; dilithium87 is legacy,
used by accounts predating the recorded scheme and by the dev-genesis accounts,
so it is supported but never chosen.

Opening the union turns every table keyed by KeypairType into a type error,
which is the point — it enumerates precisely the places needing a post-quantum
answer. Where that answer is not written yet, assertSubstrateType() expresses the
gap as a named, typed refusal at the call site rather than a silent undefined
from an object index. It throws rather than degrading because all of these paths
produce key material or an address, and a wrong-but-plausible value means funds
at an address nobody controls. Call sites disappear as quantus/common#2 and #4
land. keyFromPath throws for the same reason: silently ignoring a derivation
path would hand back the parent key under a child's name.

Quantus is appended to the registry rather than merged — @substrate/ss58-registry
does not carry it and nothing occupies prefixes 185..195, so there is nothing to
override, and appending means a registry bump cannot silently drop it.
standardAccount is 'ml-dsa', a value upstream's closed union does not allow,
hence the widened KnownSubstrate; it is load-bearing, since a missing
standardAccount makes toExpanded mark the network isIgnored.

Testnets are first-class in this fork. Upstream drops any testnet from
availableNetworks however complete its entry is, which suits a production chain
picker and not us: Quantus is under active development and a wallet that cannot
reach Heisenberg can only be tested against real funds. The exception is an
allowlist (exposedTestnet) rather than removing the rule — removing it exposed
all fifteen of upstream's testnets too, moving the sorted tail from 'ZERO' to
'ZERO Alphaville', which is a change we have no reason to make. isTestnet is
still reported honestly so consumers can label or filter; we just stop deciding
for them.

Genesis hashes come from blackbeard.observer's config, which records their
provenance: mainnet read from the node with chain_getBlockHash(0) on 2026-09-09,
its launch day, and explicitly not the staging chain that telemetry calls
"Quantus Staging Mainnet". Symbol and decimals (QTC, 12) agree between the
chain's chain_spec.rs and the Dart SDK's AppConstants.

Three upstream tests encoded rules this fork no longer follows. Each is rewritten
to assert the new rule rather than deleted:
  - genesis entries for testnets are now allowed, for ones we opted into
  - an exposed testnet must still be labelled a testnet, since a user cannot tell
    play money from real money by looking at an address
  - one network per SS58 prefix stops holding when a chain has a testnet;
    mainnet and Heisenberg are both 189 because an address is valid on either,
    and consumers key on genesisHash rather than prefix

Refs quantus/common#1

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
This commit is contained in:
rob thijssen
2026-09-10 14:50:49 +03:00
parent 9281daa14c
commit 6ed8b1ea6d
14 changed files with 237 additions and 32 deletions

View File

@@ -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);
}

View File

@@ -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));
}

View File

@@ -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([]);
});

View File

@@ -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'
],

View File

@@ -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';

View File

@@ -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'
}
];

View File

@@ -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
};

View File

@@ -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);
}
});

View File

@@ -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

View File

@@ -12,7 +12,19 @@ export type KnownLedger = Record<string, number>;
export type KnownGenesis = Record<string, HexString[]>;
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<RegistryEntry, 'standardAccount'> & {
standardAccount: RegistryEntry['standardAccount'] | 'ml-dsa';
};
export type KnownTestnet = Record<string, true>;

View File

@@ -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';

View File

@@ -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<KeypairType, DilithiumType>;
/** 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;
}

View File

@@ -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) {

View File

@@ -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 */