Compare commits

...

41 Commits

Author SHA1 Message Date
0480bfcbc5 build: packageInfo at 14.0.3-quantus.3, as published
Some checks failed
Lock Threads / lock (push) Has been cancelled
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 22:13:53 +03:00
a5157ef592 build: version the forks 14.0.3-quantus.3, on @quantus/crypto 0.3.0
The post-quantum-only change is a breaking one for every consumer. The keyring
and util-crypto surfaces both shrink, so it gets its own version rather than a
republish of quantus.2. @quantus/crypto moves to ^0.3.0, the version the
extension already resolves (wormhole addresses and nullifiers), so the two
share one copy of the WASM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 21:10:31 +03:00
e1d88c6f63 feat!: post-quantum only: remove ed25519, sr25519, ecdsa and ethereum keys
KeypairType is now 'dilithium65' | 'dilithium87'. Upstream's four types are
gone with their primitives, not merely unoffered. Each falls to Shor's
algorithm, and a keyring that can hold such a key invites someone to keep funds
under it, inside a tool whose premise is that this is unsafe.

This was deferred until post-quantum signing was proven end to end (real
transfers on Heisenberg, a real mainnet wallet in the extension), so that
tearing out half of util-crypto could not muddy the diagnosis of a first
rejected extrinsic. That has happened.

util-crypto, removed:
- ed25519/, sr25519/, secp256k1/, and signature/ (signatureVerify, which only
  knew those three; dilithiumVerify is the verifier);
- hd/ethereum and hd/ledger;
- key/fromPath and keyHdkd{Ecdsa,Ed25519,Sr25519}, the junction derivation;
- address/derive (sr25519 soft derivation);
- mnemonic/toMiniSecret, Substrate's classical seeding.

util-crypto, kept because none of it holds a key:
- ethereumEncode, isEthereumAddress and isEthereumChecksum. @polkadot/types and
  the identicon renderer format 20-byte addresses with them, and a wallet has
  to be able to show an Ethereum address to recognise and refuse one.
  ethereumEncode now refuses a secp256k1 public key, saying why.
- evm ↔ substrate address conversion (its blake2/keccak hasher moved out of
  secp256k1/ into address/), derived and multi addresses, BIP39, and suri
  parsing.

keyring:
- Only ML-DSA arms remain. The default type is dilithium65.
- Every entry point (constructor, createFromUri, createFromPair, addFromAddress
  and, above all, createFromJson for a backup the user chose) refuses a
  quantum-unsafe type with "<type> keys are not quantum-safe and cannot be held
  here", not "unknown crypto type", which reads like a bug in this software.
- pair.verify handles ML-DSA, a bare signature or signature ‖ publicKey, under
  a context that defaults to the empty raw-bytes one. Derivation and VRF
  refuse.
- The test keyring is the Quantus dev accounts (crystal_alice, dilithium_bob,
  crystal_charlie: ML-DSA-87 from seeds of 0, 1 and 2) in place of sr25519
  Alice…Ferdie and ethereum Alith…Faith.

Fixed along the way: addFromAddress passed the decoded address as a public key.
That is the same bytes on Substrate. Here it is the account id, a hash of the
key, so every watch-only account reported the hash of its own address. It now
carries the address as an account id.

Specs:
- Upstream's per-scheme keyring specs (index, pair, encode, decode, toJson,
  vrf, suri, testingPairs) are replaced by keyring.spec.ts. It covers refusals
  at every entry point (including a polkadot{.js} ed25519 JSON backup), the
  dev-account addresses, watch-only addresses, JSON round trips for both
  schemes, verify in both signature forms and failing under the wrong context
  or signer, and the absence of derivation and VRF.
- The "classical paths unchanged" pins in the ML-DSA specs are gone with the
  paths.
- The BIP39 vectors toEntropy.spec used moved from sr25519/ to
  mnemonic/bip39Vectors.spec.ts.

yarn test: 2747 passing, 0 failing. yarn lint: clean.

hw-ledger and hw-ledger-transports remain. They are device transports holding
no primitives, and nothing consumes them.

Closes #6. Refs #1: signatureVerify is gone rather than made to take a
context; dilithiumVerify is the replacement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-16 21:05:52 +03:00
7f23d0a609 fix: gate quantum-safe chains in the wallet, not in the network list
Filtering `filterAvailable` by `isQuantumSafe` removed Polkadot and Kusama from
`selectableNetworks`, and `@polkadot/types-known` throws for a chain it holds
upgrade history for but cannot find:

    Error: Unable to find info for chain kusama
      at mapRaw (@polkadot/types-known/upgrades/index.js:13:15)

which made `@polkadot/api` fail at *import* — nothing to do with Quantus, and
not diagnosable from the message.

The mistake was gating a library primitive. These lists say what Substrate
knows about; what a wallet offers is the wallet's decision, and `isQuantumSafe`
stays exported so the extension can make it in its own chain list. Keeping them
complete also means a pasted Polkadot address still decodes and renders as what
it is — recognisable, and therefore refusable. A wallet that cannot decode an
address it will not accept can only say "invalid", which reads as a typo rather
than a warning.

`index.spec.ts` is restored from upstream with only the testnet-exposure and
shared-prefix deltas re-applied, since its network-ordering assertions had
started passing vacuously once Polkadot and Kusama left the list.

Also corrects Heisenberg's token symbol to HEI; `system_properties` on
wss://a1-heisenberg.quantus.cat says HEI, not QTC.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 14:14:58 +03:00
rob thijssen
49f3442e5c build: version the forked packages 14.0.3-quantus.1, not upstream's 14.0.3
Publishing under upstream's exact version was not merely confusing in a lockfile
— it was unsafe.

@polkadot/util-crypto pins @polkadot/networks at an exact version, and
@polkadot/keyring pins util-crypto the same way. With every fork numbered
14.0.3, a consumer that forgot one resolution would have yarn quietly satisfy
that pin from npm: the *unforked* upstream package, installing cleanly, with no
Quantus network entry or no isDilithium, and nothing to indicate anything was
wrong until something failed much later for an unrelated-looking reason.

Renaming the forks makes that a resolution failure instead. Verified by deleting
the @polkadot/networks resolution in quantus/ui and running an install:

  YN0082: @polkadot/networks@npm:14.0.3-quantus.1: No candidates found

Unforked dependencies keep upstream's numbers — @polkadot/util, x-bigint,
x-randomvalues, wasm-* — because they come from npm and should.

Published with `--tag quantus`, which npm requires for a prerelease version and
which is the honest dist-tag anyway: these are not upstream's `latest`. The tag
is informational, since consumers pin by tarball URL. The old 14.0.3 artifacts
have been deleted from the registry so nothing can resolve to them.

Refs quantus/extension#2

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 11:27:59 +03:00
rob thijssen
b39241c840 build: take @quantus/crypto from the Gitea registry, not a symlink
Replaces the `link:../wasm/packages/quantus-crypto/build` resolution with an
npmScopes entry routing @quantus to https://git.lair.cafe/api/packages/quantus/npm/.

Routing the whole scope is safe here because we own every name in it. The forked
@polkadot packages cannot be handled the same way — most of that scope is
unforked and lives on npm — which is why downstream repos pin those to tarball
URLs instead.

The symlink approach had run out of road. node resolves a portal or link to its
realpath, so a consumer's transitive dependencies then resolve inside the *linked
package's* tree, where workspace entries point at source directories with no
exports map. That cost one workaround here and four in quantus/ui.

Reads from the registry are anonymous; publishing uses
`pass gitea/package-publisher-token`. Note that a version cannot be overwritten
in place — npm refuses — so republishing is delete-then-publish, which that token
can also do.

Refs quantus/extension#2

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 11:08:27 +03:00
rob thijssen
74e1e0cc6a feat(util-crypto): export quantus-cli wallet files
The counterpart to the importer, and the thing that stops it being a one-way
door. Without it an extension backup is readable only by polkadot-js-format
tooling, so a user who brought a wallet in could not take it back out.

The assumption that blocked this — that we cannot write the CLI format because we
do not store a mnemonic — turns out to be false. The format has a null-mnemonic
shape and the CLI writes one itself for every raw-seed wallet; crystal_alice,
which `quantus developer create-test-wallets` produces, has exactly that
alongside `derivation_path: "m/"`.

Verified against quantus-cli 2.2.2 rather than inferred. Exports of both schemes
were written into ~/.quantus/wallets and the CLI listed them with the correct
address, scheme and human checkphrase, showed them under `wallet view`, and
answered `export --format mnemonic` with "Mnemonic phrase is not available for
this wallet" — which is a correct statement about the file, and only reachable by
deriving the Argon2 key, opening the AES-GCM box and parsing the payload. Those
probes were removed; the committed test round-trips through our own importer,
since CI has no CLI.

What a null-mnemonic export costs the user is exactly one thing, and it is not
spending: the CLI cannot re-export a phrase from it or derive further accounts.
That is the honest consequence of never storing a phrase we showed once, and it
is the same position a raw-seed CLI wallet is already in.

Argon2 parameters match the CLI's rather than being raised. They travel in the
file and the CLI reads them, so in principle anything works — but "in principle"
is load-bearing there, and a wallet a slightly older CLI cannot open is worse
than a conservatively tuned KDF. Salt and nonce are fresh per export.

metadata.exported_by marks the file, so a CLI maintainer reading a bug report can
tell where it came from without inferring from shape.

Refs quantus/common#8

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 10:33:20 +03:00
rob thijssen
e55352aa4e feat(util-crypto): import quantus-cli wallet files
A must-have per the UX decision in quantus/extension#8: the extension has to be
able to take a wallet a user already has. My earlier recommendation against this
was withdrawn — the reasoning was that a file importer would pull the mnemonic
into storage, which is a property of an implementation rather than of the format.
This one derives and discards.

Converts at the edge rather than teaching the keyring a second container.
Decrypt (Argon2id -> AES-256-GCM), take the key material, and hand back a plain
keypair for the caller to re-encrypt as the extension's own PKCS8 under a
password the user chooses. decodePair does not learn about Argon2 and the keyring
keeps exactly one on-disk format.

The CLI stores a mnemonic in every HD wallet. This returns `hadMnemonic` and not
the phrase — the posture is that a storage password unlocks signing and nothing
on disk regenerates the tree, and an import must leave us holding exactly what
importing the same mnemonic by hand would leave us holding. Matching upstream,
which stores key material only.

No new dependencies: @noble/hashes/argon2 was already a util-crypto dependency
and AES-GCM is WebCrypto. Async only because WebCrypto has no synchronous form,
which is a second reason this belongs at the edge — createFromJson is sync and
should stay so.

Three things are refused rather than worked around:

Argon2 parameters are read from the file, not assumed. They are stored because
they are expected to change, and a build that hardcoded m=19456,t=2,p=1 would
reject a correct password on an older or newer wallet — the least useful thing it
could say.

Non-empty kyber_ciphertext/kyber_public_key, and any encryption_version other
than 2, are refused with a message. Those fields are an ML-KEM envelope mode that
is planned or optional; decrypting by the path we know and ignoring them would
fail later and more confusingly, most likely as a bad-password error.

The address sits outside the encrypted blob, so it is the one field an attacker
can edit without the password. cliWalletAddressMatches is exposed separately so
a caller can say "this file's address does not match its key" rather than
"wrong password" — a user can act on the difference.

Fixtures are genuine CLI 2.2.2 output, not blobs this repo encrypted; testing a
decoder against its own encryption proves only self-consistency. They contain no
secret: the mnemonic inside each is the public Substrate dev phrase and the
password is empty, which is the only reason committing a decryptable wallet is
acceptable. The strongest test reaches the same account two ways — through the
CLI container and by deriving from that phrase ourselves.

Refs quantus/common#7

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 09:44:07 +03:00
rob thijssen
f7dc72e32c feat(keyring,util-crypto): BIP44 derivation for ML-DSA, in place of junctions
createFromUri now derives ML-DSA pairs from the mnemonic along a hardened BIP44
path rather than from a seed along a junction chain. Lattice keys have no public
derivability, so there is no soft junction to emulate and the chain's own
Pair::derive refuses for the same reason.

The path is m/44'/189189'/<account>'/0'/<0 for 87 | 1 for 65>'. Two things about
it are unusual and both are deliberate: the account index sits at the third level
rather than the last, and the trailing index carries the scheme rather than an
address index. It is what quantus-cli and the mobile wallet already derive, so
anything else produces addresses no other Quantus tool can find.

The ML-DSA branch sits before the existing seeding, not after, because that
seeding is the wrong one for us. mnemonicToMiniSecret is Substrate's variant and
the default reach in this file; using it here yields a perfectly well-formed key
for an account nobody owns. We need the 64-byte BIP39 seed and the HMAC-SHA512
chain keyed with "Dilithium seed", which is what the hdwallet crate does.

Accepted suri forms are nothing (account 0, matching quantus-cli's default),
//<n> for an account index, and //m/44'/… for a full path. Everything else
throws. That matters more than it looks: the syntax was built for curve
junctions, where /foo is soft and //foo hard over arbitrary bytes, and quietly
reinterpreting //Alice as something would hand back an address no other tool
derives and no seed phrase obviously recovers.

A raw hex seed is taken undederived — that is how the dev-genesis accounts are
defined — and combining one with a derivation path is refused rather than
guessed, since whether such a seed is the master or already derived is
unanswerable.

pair.derive() refuses with its own message. Not "not implemented" but not
possible: a child would have to come from this pair's key material, and ML-DSA
keys are not derivable from one another at all. The caller needs the mnemonic, so
the error says so rather than being generically unhelpful.

Verified against quantus-cli for both schemes from the public Substrate dev
phrase, plus index separation, explicit paths agreeing with indices, passphrase
sensitivity, and every refusal.

Refs quantus/common#4

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 08:56:20 +03:00
rob thijssen
506b77351c fix(keyring): make account backup and restore work for ML-DSA pairs
Before this, an exported Quantus account could not be imported by anything.
decodePair located its divider by trying the two secret lengths every curve
scheme uses, 64 then 32; an ML-DSA secret is 4032 or 4896 bytes, so neither
matched and restore threw "Invalid encoding divider found in body". That is a
data-durability bug rather than a convenience one, which is why it lands before
any build lets a user create an account.

decodePair now takes an optional secretLength. The caller passes it rather than
this function searching for the divider: searching would work almost always and
fail catastrophically when it did not, since PAIR_DIV is five bytes and a
4032-byte secret contains a false match about once in 270 million keys — and the
result would be a silently wrong key rather than an error. The public key is read
as the remainder, because its length also varies and the body ends there.

The harder half is ordering. createFromJson returns a *locked* pair and callers
read pair.address off it long before any password appears. Every other scheme
manages because the address is the public key; an ML-DSA account id is a one-way
Poseidon2 hash and the public key is inside the encrypted blob. So PairInfo gains
an optional accountId, carried as data and used for the address while locked,
which decodePkcs8 clears once the real key arrives.

It also checks them against each other, and that check is not paranoia. For the
curve schemes a JSON file with an edited `address` field cannot decode at all.
Here it decodes perfectly and yields a pair reporting an address its key does not
control — a user would see someone else's address in their own wallet and believe
they held it. Pinned by a test that tampers exactly that field.

One more silent-wrong-key path closed: decodePkcs8 decided "secret key or seed?"
by length, so a 4032-byte ML-DSA secret took the seed branch and was fed to keygen
as entropy, producing a valid and entirely wrong key. The type knows the answer,
so it is asked.

Refs quantus/common#3

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 08:50:56 +03:00
rob thijssen
2bffbc62bc feat(keyring,util-crypto): ML-DSA arms for createPair, alongside the curve ones
Wires @quantus/crypto into the four TYPE_* maps, so a keyring pair of type
dilithium65/dilithium87 can be created, addressed, signed with and verified.
The curve arms are untouched: quantus/common#6 removes them, but only once
post-quantum signing is proven end to end, so that a rejected first extrinsic has
two suspects rather than three.

util-crypto gains thin dilithium/ wrappers so keyring composes primitives the way
it already does for ed25519, rather than reaching into @quantus/crypto directly.

TYPE_ADDRESS is the interesting arm. Every other entry is identity or a cheap
re-encoding because on Substrate the address *is* the public key; ML-DSA takes
1952 or 2592 bytes to 32 with no way back. dilithiumVerify therefore mirrors the
runtime's Verify::verify rather than inventing anything: split sig||pk, hash the
embedded public key, check it equals the account being verified against, and only
then check the signature. Skipping the account check would accept a valid
signature from *some* key rather than *the* key, which is the whole property.

TYPE_PREFIX gets 0 for dilithium87 and 1 for dilithium65. For the curve types
those index Substrate's MultiSignature; here they index the runtime's
DilithiumSignatureScheme — a different enum reached by the same withType
mechanism, so nothing upstream of createPair needs to know.

Signing takes a mandatory context, which is quantus/common#5 arriving early
because the two are inseparable: TYPE_SIGNATURE cannot have an ML-DSA arm without
one. There is deliberately no default. Extrinsics on spec >= 148 verify under
QUANTUS_EXTRINSIC and earlier ones under the empty context, and the wrong choice
is a valid signature the chain rejects, indistinguishable locally. Only the
caller knows the spec version.

The curve sign functions are wrapped rather than passed by reference: ed25519Sign
and sr25519Sign take a third argument of their own (onlyJs), and letting it line
up with the context slot would silently reinterpret a Uint8Array as a boolean.

VRF signing refuses for ML-DSA. The construction fakes a VRF from an ordinary
signature, which is sound only given properties ML-DSA's signature does not have;
producing something that looks like a VRF proof and cannot be verified as one is
worse than refusing.

Two runtime whitelists sat behind the KeypairType union and silently rejected the
new types — the Keyring constructor and createFromJson. The compiler cannot see
those, and the first failed at construction with a message listing four types.
Both now use one named KEYPAIR_TYPES so the next change to the union has a single
place to look and the message cannot drift from the check.

Verified against quantus-cli, not against ourselves: keyring.addFromSeed with 32
zero bytes yields crystal_alice's address, signs to the runtime's wire length,
verifies under the extrinsic context and fails under the spec-147 one, and
another account's signature is rejected for it.

Refs quantus/common#2, quantus/common#5

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-10 18:43:25 +03:00
rob thijssen
c616a0d949 feat(networks): offer only chains whose accounts are post-quantum
Deployments built on this fork should gate on chains using PQ algorithms, so
`availableNetworks` and `selectableNetworks` now exclude anything whose
standardAccount is a discrete-log scheme. *25519, Ed25519, Sr25519 and secp256k1
are all broken at once by Shor's algorithm, so a chain using one has no
quantum-safe account type to offer whatever else is true of it. A null
standardAccount counts as unsafe: unknown is not the same as safe, and being
wrong in that direction means a user holding funds under a recoverable key.

`allNetworks` is deliberately left complete. Gating what we offer must not gate
what we can read — a pasted Polkadot address has to render as a Polkadot address
so it can be recognised and refused. A wallet that cannot decode an address it
will not accept can only say "invalid", which reads as a typo rather than as a
warning.

Five upstream tests asserted the shape of a list that now holds two entries
instead of a hundred and fifty. Each is rewritten to assert the rule that
displaced it rather than deleted:
  - starting order 0/2/42 and tail 'ZERO'/'Zeitgeist' become "offers only
    post-quantum chains", plus an explicit test that non-PQ chains remain
    decodable, since that distinction is the whole design
  - the ledger slip44 check becomes its inverse: nothing offered should claim
    hardware support, because no PQ chain has a Ledger app — the devices speak
    ed25519 over the Substrate app. knownLedger is left intact rather than
    emptied so a rebase has nothing to reconcile; it simply matches nothing
  - the Polkadot field fixture becomes the Quantus one

Refs quantus/common#1

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-10 18:28:13 +03:00
rob thijssen
6ed8b1ea6d 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
2026-09-10 14:50:49 +03:00
github-actions[bot]
9281daa14c [CI Skip] release/stable 14.0.3
Some checks failed
Lock Threads / lock (push) Has been cancelled
skip-checks: true
2026-03-23 12:53:26 +00:00
Tarik Gul
c7ed69fe55 14.0.3 (#2025) 2026-03-23 14:48:13 +02:00
github-actions[bot]
22a1246672 [CI Skip] bump/beta 14.0.3-0-x
skip-checks: true
2026-03-22 21:42:26 +00:00
Francis O'Brien
4a5b6acc78 Fix(hw-ledger): reset transport on operation errors and add explicit disconnect API (#2024)
* fix(hw-ledger): close transport on withApp errors

* feat(hw-ledger): add disconnect method to close active transport connection
2026-03-22 17:37:13 -04:00
github-actions[bot]
152d4609a4 [CI Skip] release/stable 14.0.2
skip-checks: true
2026-03-16 18:24:20 +00:00
Tarik Gul
4ab957e568 14.0.2 (#2023) 2026-03-16 20:19:11 +02:00
github-actions[bot]
610c8bb15f [CI Skip] bump/beta 14.0.2-1-x
skip-checks: true
2026-03-13 20:55:22 +00:00
AndreasGassmann
372d460f1a fix(util-crypto): use correct x/y getters in secp256k1Expand (#2018) 2026-03-13 16:50:22 -04:00
github-actions[bot]
a8732a99aa [CI Skip] bump/beta 14.0.2-0-x
skip-checks: true
2026-03-12 16:59:11 +00:00
Tarik Gul
8d3cb0eb6b Set headers to 2026 (#2021) 2026-03-12 18:53:56 +02:00
github-actions[bot]
fe0886be23 [CI Skip] release/stable 14.0.1
skip-checks: true
2025-12-09 08:34:53 +00:00
rajk93
bac357ae10 14.0.1 (#2014)
* 14.0.1

* chore: update CHANGELOG.md
2025-12-09 14:01:18 +05:30
github-actions[bot]
4ab7a5bfe0 [CI Skip] bump/beta 13.5.10-1-x
skip-checks: true
2025-12-09 07:34:50 +00:00
rajk93
440b146a24 fix(x-randomvalues): prioritize native RN modules over polyfilled crypto (#2013) 2025-12-09 13:01:07 +05:30
github-actions[bot]
ae0735ccda [CI Skip] bump/beta 13.5.10-0-x
skip-checks: true
2025-12-08 13:13:55 +00:00
Paul Miller
57551b38a0 sr25519: switch from wasm to micro-sr25519 (#1971)
* sr25519: switch from wasm to micro-sr25519

* commit yarn lockfile

* Update to scure/sr25519 v0.2

* Fix build

* Commit
2025-12-08 18:40:16 +05:30
github-actions[bot]
bf63a0ebf6 [CI Skip] release/stable 13.5.9
skip-checks: true
2025-11-25 08:17:11 +00:00
rajk93
d78344d000 13.5.9 (#2012) 2025-11-25 13:43:32 +05:30
github-actions[bot]
169a702c88 [CI Skip] bump/beta 13.5.9-0-x
skip-checks: true
2025-11-25 08:02:38 +00:00
rajk93
b4d73ef49a chore: bump polkadot dependencies (#2011) 2025-11-25 13:28:57 +05:30
github-actions[bot]
21ef853fbd [CI Skip] release/stable 13.5.8
skip-checks: true
2025-11-11 06:04:48 +00:00
rajk93
a4d940cb69 13.5.8 (#2010) 2025-11-11 11:31:13 +05:30
github-actions[bot]
6423537dbe [CI Skip] bump/beta 13.5.8-0-x
skip-checks: true
2025-11-11 04:05:17 +00:00
rajk93
61125b3582 chore: bump polkadot dependencies (#2009) 2025-11-11 09:31:28 +05:30
github-actions[bot]
7d77f89f34 [CI Skip] release/stable 13.5.7
skip-checks: true
2025-10-13 14:54:53 +00:00
Valentin Fernandez
bee7204f95 13.5.7 (#2008) 2025-10-13 11:51:23 -03:00
github-actions[bot]
bb0715ec2f [CI Skip] bump/beta 13.5.7-0-x
skip-checks: true
2025-10-13 14:24:21 +00:00
Valentin Fernandez
2212f31f9a Revert "Set pbkdf2Encode rounds to default to 210,000 (#1983)" (#2007)
This reverts commit e5e6bdd814.
2025-10-13 11:20:32 -03:00
642 changed files with 50634 additions and 4916 deletions

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util authors & contributors
// Copyright 2017-2026 @polkadot/util authors & contributors
// SPDX-License-Identifier: Apache-2.0
module.exports = require('@polkadot/dev/config/prettier.cjs');

View File

@@ -13,3 +13,12 @@ logFilters:
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.9.1.cjs
# @quantus packages come from the Gitea registry. Safe as a whole-scope route
# because we own every name in it — unlike @polkadot, where routing the scope
# would send yarn looking for dozens of unforked packages that are not there.
#
# Reads are anonymous; publishing needs `pass gitea/package-publisher-token`.
npmScopes:
quantus:
npmRegistryServer: "https://git.lair.cafe/api/packages/quantus/npm/"

View File

@@ -1,5 +1,52 @@
# CHANGELOG
## 14.0.3 Mar 23, 2026
Changes:
- Fix(hw-ledger): reset transport on operation errors and add explicit disconnect API ([#2024](https://github.com/polkadot-js/common/pull/2024))
## 14.0.2 Mar 16, 2026
Changes:
- Set headers to 2026 ([#2021](https://github.com/polkadot-js/common/pull/2021))
Contributed:
- fix(util-crypto): use correct x/y getters in secp256k1Expand ([#2018](https://github.com/polkadot-js/common/pull/2018)) (Thanks to https://github.com/AndreasGassmann)
## 14.0.1 Dec 9, 2025
Changes:
- fix(x-randomvalues): prioritize native RN modules over polyfilled crypto ([#2013](https://github.com/polkadot-js/common/pull/2013))
- sr25519: switch from wasm to micro-sr25519 ([#1971](https://github.com/polkadot-js/common/pull/1971))
## 13.5.9 Nov 25, 2025
Changes:
- Bump polkadot dependencies ([#2011](https://github.com/polkadot-js/common/pull/2011))
## 13.5.8 Nov 11, 2025
Changes:
- Bump @polkadot dependencies ([#2009](https://github.com/polkadot-js/common/pull/2009))
## 13.5.7 Oct 13, 2025
Changes:
- Revert "Set pbkdf2Encode rounds to default to 210,000" ([#2007](https://github.com/polkadot-js/common/pull/2007))
## 13.5.6 Aug 26, 2025
Changes:

View File

@@ -1,17 +1,17 @@
1674 Jaco Bump deps (#1912)
44 Tarik Gul Set pbkdf2Encode rounds to default to 210,000 (#1983)
47 Tarik Gul 14.0.3 (#2025)
13 Carlo Sala feat(networks): add zeitgeist Ledger support (#1840)
11 Valentin Fernandez 13.5.7 (#2008)
9 Antoine Estienne Add eth uri keyring test (#1291)
9 Valentin Fernandez Bump `@polkadot/wasm` deps (#2002)
9 rajk93 14.0.1 (#2014)
6 Amaury Martiny Add tests from Rust (#283)
6 Jakub Pánik HydraDX -> Hydration rebrand (#1923)
5 Arjun Porwal 13.5.6 (#2004)
5 kwingram25 Add BN consts and timeToString (#610)
4 Adam Dossa Update Polymesh Genesis Hash (#1193)
4 pan add bifrost kusama (#1626)
3 AndreasGassmann fix(util-crypto): use correct x/y getters in secp256k1Expand (#2018)
3 Luke Schoen docs: Fix example in readme since we use default imports (#243)
3 rajk93 13.5.3 (#1996)
2 AndreasGassmann feat(dependencies): remove moment.js (#530)
2 Cameron Fairchild Add bittensor details (#1783)
2 Gav Wood Fix stake:unstake (#182)
2 Hyungsuk Kang Add cross client Encryption/Decryption (#496)
@@ -32,6 +32,7 @@
1 Chris Get rid of warnings for dual esm and cjs packages of the same version (#1890)
1 Dmitrii Novikov Add `Vara` support to Generic Ledger App (#1958)
1 Eliott Teissonniere add nodle ledger specs (#1089)
1 Francis O'Brien Fix(hw-ledger): reset transport on operation errors and add explicit disconnect API (#2024)
1 Guilane DENGUIR feat(VTB): Added support for VTB blockchain (#1651)
1 h4x3rotab Throws an error when signing with a locked keypair (#578)
1 Harry Liu Update README.md (#446)
@@ -54,6 +55,7 @@
1 Mohsan Riaz Add Ternoa for ledger hardware support (#1711)
1 nahuseyoum Reserving 65 for Aventus based on the new extended range (#871)
1 Nantian add acala ledger config (#1485)
1 Paul Miller sr25519: switch from wasm to micro-sr25519 (#1971)
1 peetzweg/ adds `isRiscV` guard function similar to isWasm (#1878)
1 qwer951123 Add acala genesis (#1161)
1 Radek Bochenek feat: add Liberland support to Generic Ledger App (#1989)

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util authors & contributors
// Copyright 2017-2026 @polkadot/util authors & contributors
// SPDX-License-Identifier: Apache-2.0
import baseConfig from '@polkadot/dev/config/eslint';

View File

@@ -14,10 +14,10 @@
},
"sideEffects": false,
"type": "module",
"version": "13.5.6",
"version": "14.0.3",
"versions": {
"git": "13.5.6",
"npm": "13.5.6"
"git": "14.0.3",
"npm": "14.0.3"
},
"workspaces": [
"packages/*"
@@ -31,8 +31,7 @@
"deno": "yarn polkadot-dev-deno-map && yarn build && deno check --import-map=import_map.json mod.ts",
"lint": "polkadot-dev-run-lint",
"postinstall": "polkadot-dev-yarn-only",
"test": "polkadot-dev-run-test --env browser ^mnemonic/toMiniSecretCmp",
"test:mnemonicCmp": "polkadot-dev-run-test --env browser mnemonic/toMiniSecretCmp",
"test": "polkadot-dev-run-test --env browser",
"test:node": "polkadot-dev-run-test --env browser ^mnemonic/toMiniSecretCmp",
"test:one": "polkadot-dev-run-test --env browser"
},

View File

@@ -14,7 +14,7 @@
},
"sideEffects": false,
"type": "module",
"version": "13.5.6",
"version": "14.0.3",
"browser": "browser.js",
"main": "node.js",
"react-native": "react-native.js",
@@ -22,7 +22,7 @@
"@ledgerhq/hw-transport": "^6.31.4",
"@ledgerhq/hw-transport-webhid": "^6.29.4",
"@ledgerhq/hw-transport-webusb": "^6.29.4",
"@polkadot/util": "13.5.6",
"@polkadot/util": "14.0.3",
"tslib": "^2.8.0"
},
"optionalDependencies": {

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
import LedgerHid from '@ledgerhq/hw-transport-webhid';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { createDefs } from './util.js';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './empty.js';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
import LedgerHid from '@ledgerhq/hw-transport-node-hid-singleton';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger-transports authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger-transports authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @polkadot/dev

View File

@@ -1,6 +1,6 @@
// Copyright 2017-2025 @polkadot/hw-ledger-transports authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger-transports authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @polkadot/dev
export const packageInfo = { name: '@polkadot/hw-ledger-transports', path: 'auto', type: 'auto', version: '13.5.6' };
export const packageInfo = { name: '@polkadot/hw-ledger-transports', path: 'auto', type: 'auto', version: '14.0.3' };

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './empty.js';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
// CJS, so we use import * syntax

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Transport, TransportDef, TransportType } from './types.js';

View File

@@ -17,11 +17,11 @@
"./packageDetect.cjs"
],
"type": "module",
"version": "13.5.6",
"version": "14.0.3",
"main": "index.js",
"dependencies": {
"@polkadot/hw-ledger-transports": "13.5.6",
"@polkadot/util": "13.5.6",
"@polkadot/hw-ledger-transports": "14.0.3",
"@polkadot/util": "14.0.3",
"@zondax/ledger-substrate": "1.1.1",
"tslib": "^2.8.0"
}

View File

@@ -1,8 +1,8 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { SubstrateApp } from '@zondax/ledger-substrate';
import type { TransportDef, TransportType } from '@polkadot/hw-ledger-transports/types';
import type { Transport, TransportDef, TransportType } from '@polkadot/hw-ledger-transports/types';
import type { AccountOptions, LedgerAddress, LedgerSignature, LedgerVersion } from './types.js';
import { newSubstrateApp } from '@zondax/ledger-substrate';
@@ -30,6 +30,17 @@ async function wrapError <T extends WrappedResult> (promise: Promise<T>): Promis
return result;
}
/** @internal Best-effort transport cleanup after failed operations */
async function closeTransport (transport: Transport | null): Promise<void> {
if (transport) {
try {
await transport.close();
} catch {
// Ignore cleanup errors so the original failure is preserved.
}
}
}
/** @internal Wraps a sign/signRaw call and returns the associated signature */
function sign (method: 'sign' | 'signRaw', message: Uint8Array, accountOffset = 0, addressOffset = 0, { account = LEDGER_DEFAULT_ACCOUNT, addressIndex = LEDGER_DEFAULT_INDEX, change = LEDGER_DEFAULT_CHANGE }: Partial<AccountOptions> = {}): (app: SubstrateApp) => Promise<LedgerSignature> {
return async (app: SubstrateApp): Promise<LedgerSignature> => {
@@ -114,16 +125,29 @@ export class Ledger {
return this.withApp(sign('signRaw', u8aWrapBytes(message), accountOffset, addressOffset, options));
}
/**
* Closes any active transport connection
*/
public async disconnect (): Promise<void> {
const app = this.#app;
this.#app = null;
await closeTransport(app?.transport || null);
}
/**
* @internal
*
* Returns a created SubstrateApp to perform operations against. Generally
* this is only used internally, to ensure consistent bahavior.
* this is only used internally, to ensure consistent behavior.
*/
async withApp <T> (fn: (app: SubstrateApp) => Promise<T>): Promise<T> {
let transport: Transport | null = null;
try {
if (!this.#app) {
const transport = await this.#transportDef.create();
transport = await this.#transportDef.create();
// We need this override for the actual type passing - the Deno environment
// is quite a bit stricter and it yields invalids between the two (specifically
@@ -136,6 +160,7 @@ export class Ledger {
return await fn(this.#app);
} catch (error) {
await closeTransport(this.#app?.transport || transport);
this.#app = null;
throw error;

View File

@@ -1,7 +1,7 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { TransportDef, TransportType } from '@polkadot/hw-ledger-transports/types';
import type { Transport, TransportDef, TransportType } from '@polkadot/hw-ledger-transports/types';
import type { AccountOptionsGeneric, LedgerAddress, LedgerSignature, LedgerVersion } from './types.js';
import { PolkadotGenericApp } from '@zondax/ledger-substrate';
@@ -33,7 +33,7 @@ async function wrapError <T extends WrappedResult> (promise: Promise<T>): Promis
try {
result = await promise;
} catch (e: unknown) {
// We check to see if the propogated error is the newer ResponseError type.
// We check to see if the propagated error is the newer ResponseError type.
// The response code use to be part of the result, but with the latest breaking changes from 0.42.x
// the interface and it's types have completely changed.
if ((e as ResponseError).returnCode) {
@@ -46,6 +46,17 @@ async function wrapError <T extends WrappedResult> (promise: Promise<T>): Promis
return result;
}
/** @internal Best-effort transport cleanup after failed operations */
async function closeTransport (transport: Transport | null): Promise<void> {
if (transport) {
try {
await transport.close();
} catch {
// Ignore cleanup errors so the original failure is preserved.
}
}
}
/** @internal Wraps a signEd25519/signRawEd25519 call and returns the associated signature */
function sign (method: 'signEd25519' | 'signRawEd25519', message: Uint8Array, slip44: number, accountIndex = 0, addressOffset = 0): (app: PolkadotGenericApp) => Promise<LedgerSignature> {
const bip42Path = `m/44'/${slip44}'/${accountIndex}'/${0}'/${addressOffset}'`;
@@ -245,6 +256,17 @@ export class LedgerGeneric {
return this.withApp(signWithMetadataEcdsa(message, this.#slip44, accountIndex, addressOffset, options));
}
/**
* Closes any active transport connection
*/
public async disconnect (): Promise<void> {
const app = this.#app;
this.#app = null;
await closeTransport(app?.transport || null);
}
/**
* @internal
*
@@ -252,9 +274,11 @@ export class LedgerGeneric {
* this is only used internally, to ensure consistent bahavior.
*/
async withApp <T> (fn: (app: PolkadotGenericApp) => Promise<T>): Promise<T> {
let transport: Transport | null = null;
try {
if (!this.#app) {
const transport = await this.#transportDef.create();
transport = await this.#transportDef.create();
// We need this override for the actual type passing - the Deno environment
// is quite a bit stricter and it yields invalids between the two (specifically
@@ -267,6 +291,7 @@ export class LedgerGeneric {
return await fn(this.#app);
} catch (error) {
await closeTransport(this.#app?.transport || transport);
this.#app = null;
throw error;

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
// This is necessary to ensure users still have access to class Ledger even though its deprecated.

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
export const LEDGER_DEFAULT_ACCOUNT = 0x80000000;

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
// These map to the known name in the @zondax/ledger-substrate/supported_apps package

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
import './packageDetect.js';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './index.js';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @polkadot/dev

View File

@@ -1,6 +1,6 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @polkadot/dev
export const packageInfo = { name: '@polkadot/hw-ledger', path: 'auto', type: 'auto', version: '13.5.6' };
export const packageInfo = { name: '@polkadot/hw-ledger', path: 'auto', type: 'auto', version: '14.0.3' };

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/hw-ledger authors & contributors
// Copyright 2017-2026 @polkadot/hw-ledger authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@polkadot/util/types';

View File

@@ -18,15 +18,16 @@
"./packageDetect.cjs"
],
"type": "module",
"version": "13.5.6",
"version": "14.0.3-quantus.3",
"main": "index.js",
"dependencies": {
"@polkadot/util": "13.5.6",
"@polkadot/util-crypto": "13.5.6",
"@polkadot/util": "14.0.3",
"@polkadot/util-crypto": "14.0.3-quantus.3",
"@quantus/crypto": "^0.3.0",
"tslib": "^2.8.0"
},
"peerDependencies": {
"@polkadot/util": "13.5.6",
"@polkadot/util-crypto": "13.5.6"
"@polkadot/util": "14.0.3",
"@polkadot/util-crypto": "14.0.3"
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
// all external

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
// default substrate dev phrase

View File

@@ -1,609 +0,0 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import type { KeyringPair$Json } from './types.js';
import { hexToU8a, stringToU8a } from '@polkadot/util';
import { base64Decode, cryptoWaitReady, encodeAddress, mnemonicGenerate, randomAsU8a, setSS58Format } from '@polkadot/util-crypto';
import * as languages from '@polkadot/util-crypto/mnemonic/wordlists/index';
import { decodePair } from './pair/decode.js';
import Keyring from './index.js';
await cryptoWaitReady();
describe('keypair', (): void => {
describe('ed25519', (): void => {
const publicKeyOne = new Uint8Array([47, 140, 97, 41, 216, 22, 207, 81, 195, 116, 188, 127, 8, 195, 230, 62, 209, 86, 207, 120, 174, 251, 74, 101, 80, 217, 123, 135, 153, 121, 119, 238]);
const publicKeyTwo = new Uint8Array([215, 90, 152, 1, 130, 177, 10, 183, 213, 75, 254, 211, 201, 100, 7, 58, 14, 225, 114, 243, 218, 166, 35, 37, 175, 2, 26, 104, 247, 7, 81, 26]);
const seedOne = stringToU8a('12345678901234567890123456789012');
const seedTwo = hexToU8a('0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60');
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42, type: 'ed25519' });
keyring.addFromSeed(seedOne, {});
});
it('adds the pair', (): void => {
expect(
keyring.addFromSeed(seedTwo, {}).publicKey
).toEqual(publicKeyTwo);
});
it('creates via a dev seed', (): void => {
expect(
keyring.addFromUri('//Alice').address
).toEqual('5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu');
});
it('creates a ed25519 pair via mnemonicToSeed', (): void => {
expect(
keyring.addFromUri(
'seed sock milk update focus rotate barely fade car face mechanic mercy'
).address
).toEqual('5DkQP32jP4DVJLWWBRBoZF2tpWjqFrcrTBo6H5NcSk7MxKCC');
});
it('adds from a mnemonic, with correct ss58', (): void => {
// eslint-disable-next-line deprecation/deprecation
setSS58Format(20); // this would not be used
keyring.setSS58Format(2); // this would be used
const pair = keyring.addFromMnemonic('moral movie very draw assault whisper awful rebuild speed purity repeat card', {});
expect(pair.address).toEqual('HSLu2eci2GCfWkRimjjdTXKoFSDL3rBv5Ey2JWCBj68cVZj');
expect(encodeAddress(pair.publicKey)).toEqual('35cDYtPsdG1HUa2n2MaARgJyRz1WKMBZK1DL6c5cX7nugQh1');
});
it('allows publicKeys retrieval', (): void => {
keyring.addFromSeed(seedTwo, {});
expect(
keyring.getPublicKeys()
).toEqual([publicKeyOne, publicKeyTwo]);
});
it('allows retrieval of a specific item', (): void => {
expect(
keyring.getPair(publicKeyOne).publicKey
).toEqual(publicKeyOne);
});
it('allows adding from JSON', (): void => {
expect(
keyring.addFromJson(
JSON.parse('{"address":"5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua","encoded":"0xb4a14995d25ab609f3686e9fa45f1fb237cd833f33f00d4b12c51858ca070d96972e47d73aae5eeb0fc06f923826cf0943fdb02c2c2ee30ef52a7912663053940d1da4da66b3a3f520ae07422c1c94b2d95690fca9d1f4a997623bb2923a8833280e19e7f72c3c5cfa343974e60e2b3dc53b404fdaf330756daad5e4e3","encoding":{"content":"pkcs8","type":"xsalsa20-poly1305","version":"0"},"meta":{"isTesting":true,"name":"alice"}}') as KeyringPair$Json
).publicKey
).toEqual(
new Uint8Array([209, 114, 167, 76, 218, 76, 134, 89, 18, 195, 43, 160, 168, 10, 87, 174, 105, 171, 174, 65, 14, 92, 203, 89, 222, 232, 78, 47, 68, 50, 219, 79])
);
});
it('signs and verifies', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(publicKeyOne);
const signature = pair.sign(MESSAGE);
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
it('signs and verifies (withType)', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(publicKeyOne);
const signature = pair.sign(MESSAGE, { withType: true });
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
});
describe('sr25519', (): void => {
const publicKeyOne = new Uint8Array([116, 28, 8, 160, 111, 65, 197, 150, 96, 143, 103, 116, 37, 155, 217, 4, 51, 4, 173, 250, 93, 62, 234, 98, 118, 11, 217, 190, 151, 99, 77, 99]);
const publicKeyTwo = hexToU8a('0x44a996beb1eef7bdcab976ab6d2ca26104834164ecf28fb375600576fcc6eb0f');
const seedOne = stringToU8a('12345678901234567890123456789012');
const seedTwo = hexToU8a('0x9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60');
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42, type: 'sr25519' });
keyring.addFromSeed(seedOne, {});
});
it('creates with dev phrase when only path specified', (): void => {
expect(
keyring.createFromUri('//Alice').address
).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
});
it('creates with integer derivations', (): void => {
// MAX_SAFE_INTEGER
expect(
keyring.createFromUri('//9007199254740991').address
).toEqual('5CDsyNZyqxLpHnTvknr68anUcYoBFjZbFKiEJJf4prB75Uog');
// MAX_SAFE_INTEGER + extra digits
expect(
keyring.createFromUri('//900719925474099999').address
).toEqual('5GHj2D7RG2m2DXYwGSDpXwuuxn53G987i7p2EQVDqP4NYu4q');
});
it('creates via dev seed (2-byte encoding)', (): void => {
keyring.setSS58Format(252);
expect(
keyring.addFromUri('//Alice').address
).toEqual('xw8P6urbSAronL3zZFB7dg8p7LLSgKCUFDUgjohnf1iP434ic');
});
it('adds the pair', (): void => {
expect(
keyring.addFromSeed(seedTwo, {}).publicKey
).toEqual(publicKeyTwo);
});
it('adds from a mnemonic', (): void => {
keyring.setSS58Format(2);
expect(
keyring.addFromMnemonic('moral movie very draw assault whisper awful rebuild speed purity repeat card', {}).address
).toEqual('FSjXNRT2K1R5caeHLPD6WMrqYUpfGZB7ua8W89JFctZ1YqV');
});
it('allows publicKeys retrieval', (): void => {
keyring.addFromSeed(seedTwo, {});
expect(
keyring.getPublicKeys()
).toEqual([publicKeyOne, publicKeyTwo]);
});
it('allows retrieval of a specific item', (): void => {
expect(
keyring.getPair(publicKeyOne).publicKey
).toEqual(publicKeyOne);
});
it('allows adding from JSON', (): void => {
expect(
keyring.addFromJson(
JSON.parse('{"address":"5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua","encoded":"0xb4a14995d25ab609f3686e9fa45f1fb237cd833f33f00d4b12c51858ca070d96972e47d73aae5eeb0fc06f923826cf0943fdb02c2c2ee30ef52a7912663053940d1da4da66b3a3f520ae07422c1c94b2d95690fca9d1f4a997623bb2923a8833280e19e7f72c3c5cfa343974e60e2b3dc53b404fdaf330756daad5e4e3","encoding":{"content":"pkcs8","type":"xsalsa20-poly1305","version":"0"},"meta":{"isTesting":true,"name":"alice"}}') as KeyringPair$Json
).publicKey
).toEqual(
new Uint8Array([209, 114, 167, 76, 218, 76, 134, 89, 18, 195, 43, 160, 168, 10, 87, 174, 105, 171, 174, 65, 14, 92, 203, 89, 222, 232, 78, 47, 68, 50, 219, 79])
);
});
it('signs and verifies', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(publicKeyOne);
const signature = pair.sign(MESSAGE);
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
it('signs and verifies (withType)', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(publicKeyOne);
const signature = pair.sign(MESSAGE, { withType: true });
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
});
describe('ecdsa', (): void => {
const seedOne = 'potato act energy ahead stone taxi receive fame gossip equip chest round';
const seedTwo = hexToU8a('0x3c74be003bd9a876be439949ccf2b292bd966c94959a689173b295b326cd6da7');
const publicKeyOne = hexToU8a('0x02c6b6c664db5ef505477bba1cf2f1789c98796b9bb5fa21abd0ac4589bed980e7');
const publicKeyTwo = hexToU8a('0x021da683b913fb28c979ba3e5f1881415cef4b1f58a5d05ed3610a2995e7b4943c');
const addressKeyOne = hexToU8a('0x0cfd0dd2c59a9987b9848919163931b6a42283ffd3d91e92c98b522525a7038f');
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42, type: 'ecdsa' });
keyring.addFromMnemonic(seedOne, {});
});
it('creates with dev phrase when only path specified', (): void => {
expect(
keyring.createFromUri('//Alice').address
).toEqual('5C7C2Z5sWbytvHpuLTvzKunnnRwQxft1jiqrLD5rhucQ5S9X');
});
it('adds the pair', (): void => {
expect(
keyring.addFromSeed(seedTwo, {}).publicKey
).toEqual(publicKeyTwo);
});
it('adds from a mnemonic', (): void => {
keyring.setSS58Format(2);
expect(
keyring.addFromMnemonic('moral movie very draw assault whisper awful rebuild speed purity repeat card').address
).toEqual('DrRE1KAcs4pCicX8yJPh7YxkLPQ2vXnCFSVRPQfx38KjEFe');
});
it('allows publicKeys retrieval', (): void => {
keyring.addFromSeed(seedTwo, {});
expect(
keyring.getPublicKeys()
).toEqual([publicKeyOne, publicKeyTwo]);
});
it('allows retrieval of a specific item', (): void => {
expect(
keyring.getPair(addressKeyOne).publicKey
).toEqual(publicKeyOne);
});
it('allows adding from JSON', (): void => {
expect(
keyring.addFromJson(
JSON.parse('{"address":"5DzMsaYFhmpRdErWrP6K6PD7UXzYoeETToSBUrZSvxasqWRz","encoded":"0xa192d39b42bc1601bf61df31039a554228593fadf870bc837b658a5114627aca199fff596260c95fe8994c66a47636cf0270aa08f402ba5541038753960d00e6c3af5e239ec58fb1eef3db7d6bc266f4853bdfe4ed17122d9092d879014d53980d2ee57f6f55a88c38836447d8645008e8815379626addc8f81f80cd49a2","encoding":{"content":"pkcs8","type":"xsalsa20-poly1305","version":"2"},"meta":{}}') as KeyringPair$Json
).address
).toEqual('5DzMsaYFhmpRdErWrP6K6PD7UXzYoeETToSBUrZSvxasqWRz');
});
it('allows creation from JSON', (): void => {
keyring.setSS58Format(2);
const pair = keyring.createFromJson(
JSON.parse('{"address":"0x02fde629668eb2bcc7d748f40a7e597f7c7b363498ff3db31f03ce4854937883ad","encoded":"qIhAhKqtf2iyEoWEr8nmBdksSI8EHHCpgJHToqd6Pl8AgAAAAQAAAAgAAADDZ//fj/BRRj+0+bl1KAlYgoPJp6nEUwiw0fVqO2BW4mjEgQ+iWwJEgDf1JUtecbzOlfhTXBzqX/dIYzLgUADrF4EFEPpboCWiU1iN7W/3DM1cOTRVvTGcbdIqW//z3axhz961qzeJVUIFgllwGe/euLUPIlKbIkiN/CsRYdQ=","encoding":{"content":["pkcs8","ecdsa"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"ecdsa","tags":[],"whenCreated":1600925898271}}') as KeyringPair$Json
);
expect(pair.address).toEqual('DHL8HKFuTTR55JzzLmkJRCAfPBbuevKaT9cXikxbEV97Ko8');
expect(pair.publicKey).toEqual(hexToU8a('0x02fde629668eb2bcc7d748f40a7e597f7c7b363498ff3db31f03ce4854937883ad'));
});
it('fails toJson() when password is incorrect', (): void => {
const pair = keyring.createFromJson(
JSON.parse('{"address":"0x02fde629668eb2bcc7d748f40a7e597f7c7b363498ff3db31f03ce4854937883ad","encoded":"qIhAhKqtf2iyEoWEr8nmBdksSI8EHHCpgJHToqd6Pl8AgAAAAQAAAAgAAADDZ//fj/BRRj+0+bl1KAlYgoPJp6nEUwiw0fVqO2BW4mjEgQ+iWwJEgDf1JUtecbzOlfhTXBzqX/dIYzLgUADrF4EFEPpboCWiU1iN7W/3DM1cOTRVvTGcbdIqW//z3axhz961qzeJVUIFgllwGe/euLUPIlKbIkiN/CsRYdQ=","encoding":{"content":["pkcs8","ecdsa"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"ecdsa","tags":[],"whenCreated":1600925898271}}') as KeyringPair$Json
);
expect(
() => pair.toJson('invalid')
).toThrow(/Unable to decode using the supplied passphrase/);
});
it('pass toJson() when password is correct', (): void => {
const pair = keyring.createFromJson(
JSON.parse('{"address":"0x02fde629668eb2bcc7d748f40a7e597f7c7b363498ff3db31f03ce4854937883ad","encoded":"qIhAhKqtf2iyEoWEr8nmBdksSI8EHHCpgJHToqd6Pl8AgAAAAQAAAAgAAADDZ//fj/BRRj+0+bl1KAlYgoPJp6nEUwiw0fVqO2BW4mjEgQ+iWwJEgDf1JUtecbzOlfhTXBzqX/dIYzLgUADrF4EFEPpboCWiU1iN7W/3DM1cOTRVvTGcbdIqW//z3axhz961qzeJVUIFgllwGe/euLUPIlKbIkiN/CsRYdQ=","encoding":{"content":["pkcs8","ecdsa"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"ecdsa","tags":[],"whenCreated":1600925898271}}') as KeyringPair$Json
);
expect(
() => pair.toJson('testing')
).not.toThrow();
});
it('encodes a pair toJSON (and decodes)', (): void => {
const pair = keyring.createFromUri('moral movie very draw assault whisper awful rebuild speed purity repeat card');
const json = pair.toJson('password');
expect(json.address).toEqual('0x03ddca309bd5fedd01f914d6fb76f23aa848a2a520802159215dba5085d7863619');
expect(json.encoding).toEqual({
content: ['pkcs8', 'ecdsa'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
});
const newPair = keyring.createFromJson(json);
expect(newPair.publicKey).toEqual(pair.publicKey);
expect(
() => newPair.unlock('password')
).not.toThrow();
});
it('signs and verifies', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(addressKeyOne);
const signature = pair.sign(MESSAGE);
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
it('signs and verifies (withType)', (): void => {
const MESSAGE = stringToU8a('this is a message');
const pair = keyring.getPair(addressKeyOne);
const signature = pair.sign(MESSAGE, { withType: true });
expect(pair.verify(MESSAGE, signature, pair.publicKey)).toBe(true);
expect(pair.verify(MESSAGE, signature, randomAsU8a())).toBe(false);
expect(pair.verify(new Uint8Array(), signature, pair.publicKey)).toBe(false);
});
});
describe('ethereum', (): void => {
// combine mnemonic with derivation path
const PHRASE = 'seed sock milk update focus rotate barely fade car face mechanic mercy' + '/m/44\'/60\'/0\'/0/0';
const PRIV_KEY_ONE = '0x070dc3117300011918e26b02176945cc15c3d548cf49fd8418d97f93af699e46';
const ETH_ADDRESS_ONE = '0x31ea8795EE32D782C8ff41a5C68Dcbf0F5B27f6d';
const ETH_ADDRESS_TWO = '0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887';
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ type: 'ethereum' });
});
it('creates with dev phrase from the private key', (): void => {
const pair = keyring.addFromSeed(hexToU8a(PRIV_KEY_ONE));
expect(
pair.address
).toEqual(ETH_ADDRESS_ONE);
});
it('creates with dev phrase from the private key in createFromUri', (): void => {
const pair = keyring.createFromUri(PRIV_KEY_ONE);
expect(
pair.address
).toEqual(ETH_ADDRESS_ONE);
});
it('creates with dev phrase with derivation path specified', (): void => {
const pair = keyring.createFromUri(PHRASE, undefined, undefined, undefined, 2048);
expect(
pair.address
).toEqual(ETH_ADDRESS_ONE);
});
it('creates with dev phrase with derivation path specified - addFromUri', (): void => {
expect(
keyring.addFromUri(PHRASE, undefined, undefined, undefined, 2048).address
).toEqual(ETH_ADDRESS_ONE);
});
it('creates with dev phrase with derivation path specified - addFromUri with type', (): void => {
const keyringUntyped = new Keyring();
expect(
keyringUntyped.addFromUri(PHRASE, {}, 'ethereum', undefined, 2048).address
).toEqual(ETH_ADDRESS_ONE);
});
it('encodes a pair toJSON (and decodes)', (): void => {
const pair = keyring.createFromUri(PHRASE, undefined, undefined, undefined, 2048);
const json = pair.toJson('password');
expect(json.address).toEqual('0x0381351b1b46d2602b0992bb5d5531f9c1696b0812feb2534b6884adc47e2e1d8b'); // this is the public key (different from address for ethereum)
expect(json.encoding).toEqual({
content: ['pkcs8', 'ethereum'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
});
const newPair = keyring.createFromJson(json);
expect(newPair.publicKey).toEqual(pair.publicKey);
expect(
() => newPair.unlock('password')
).not.toThrow();
});
it('encodes a pair toJSON and back', (): void => {
const pairOriginal = keyring.createFromUri(PHRASE, undefined, undefined, undefined, 2048);
const json = pairOriginal.toJson('password');
const pair = keyring.addFromJson(
json
);
expect(pair.address).toEqual(ETH_ADDRESS_ONE);
pair.decodePkcs8('password');
expect(pair.isLocked).toBe(false);
expect(pair.address).toBe(ETH_ADDRESS_ONE);
});
it('allows adding from JSON', (): void => {
const pair = keyring.addFromJson(
JSON.parse('{"address":"KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou","encoded":"U8qFEaghhmNV2PgFhjqzmhyUy37Ok7abfFU2MNsBd0sAgAAAAQAAAAgAAAA3+NniKogzNphiMNueB1X0sGA07B6CaXWfpXPx45iSXoTTprwzU5mOoSqUWO0GKHROI72LN+uJ8Yfv6Ll6JOOV3VPKfoVoFmYm+zDrrMPa0gk5E5kUuSijxADcE6zUrliPVr0Ix/qaghu5SJ7RtWDQLBf4Hp86SJ8Gg6gTSSk=","encoding":{"content":["pkcs8","ethereum"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{}}') as KeyringPair$Json
);
expect(pair.publicKey).toEqual(hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077'));
expect(pair.address).toEqual(ETH_ADDRESS_TWO);
pair.decodePkcs8('password');
expect(pair.isLocked).toBe(false);
expect(pair.publicKey).toEqual(hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077'));
expect(pair.address).toBe(ETH_ADDRESS_TWO);
});
it('allows for signing/verification', (): void => {
const MESSAGE = stringToU8a('just some test message');
const signer = keyring.createFromUri(PHRASE);
const verifier = keyring.addFromJson(
JSON.parse('{"address":"KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou","encoded":"U8qFEaghhmNV2PgFhjqzmhyUy37Ok7abfFU2MNsBd0sAgAAAAQAAAAgAAAA3+NniKogzNphiMNueB1X0sGA07B6CaXWfpXPx45iSXoTTprwzU5mOoSqUWO0GKHROI72LN+uJ8Yfv6Ll6JOOV3VPKfoVoFmYm+zDrrMPa0gk5E5kUuSijxADcE6zUrliPVr0Ix/qaghu5SJ7RtWDQLBf4Hp86SJ8Gg6gTSSk=","encoding":{"content":["pkcs8","ethereum"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{}}') as KeyringPair$Json
);
const signature = signer.sign(MESSAGE);
const dummyPublic = verifier.publicKey.slice();
dummyPublic[dummyPublic.length - 1] = 0;
expect(verifier.verify(MESSAGE, signature, signer.publicKey)).toBe(true);
expect(verifier.verify(MESSAGE, signature, dummyPublic)).toBe(false);
expect(verifier.verify(new Uint8Array(), signature, signer.publicKey)).toBe(false);
});
it('allows for signing/verification (withType)', (): void => {
const MESSAGE = stringToU8a('just some test message');
const signer = keyring.createFromUri(PHRASE);
const verifier = keyring.addFromJson(
JSON.parse('{"address":"KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou","encoded":"U8qFEaghhmNV2PgFhjqzmhyUy37Ok7abfFU2MNsBd0sAgAAAAQAAAAgAAAA3+NniKogzNphiMNueB1X0sGA07B6CaXWfpXPx45iSXoTTprwzU5mOoSqUWO0GKHROI72LN+uJ8Yfv6Ll6JOOV3VPKfoVoFmYm+zDrrMPa0gk5E5kUuSijxADcE6zUrliPVr0Ix/qaghu5SJ7RtWDQLBf4Hp86SJ8Gg6gTSSk=","encoding":{"content":["pkcs8","ethereum"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{}}') as KeyringPair$Json
);
const signature = signer.sign(MESSAGE, { withType: true });
const dummyPublic = verifier.publicKey.slice();
dummyPublic[dummyPublic.length - 1] = 0;
expect(verifier.verify(MESSAGE, signature, signer.publicKey)).toBe(true);
expect(verifier.verify(MESSAGE, signature, dummyPublic)).toBe(false);
expect(verifier.verify(new Uint8Array(), signature, signer.publicKey)).toBe(false);
});
});
describe('raw pair add/create', (): void => {
const json = JSON.parse('{"address":"5PjeoaQzCoYbSi42aQRKB3Sx18StCaEAzCbGEEbWbZyfKS3H","encoded":"JQUl8ZpoXv2OMkL9TPylLmcIye2cYhaS9INICbFgZTsAgAAAAQAAAAgAAAAr/0hJOOzokIdBG71TstigLABX9D5xGD7L37ySxtjDrVRg26LL90jLQ47quT9o3bq6ppXMVL6USk7Q4p3WU66bojTFuCDyhpYRhNbUqU6s0rD3S4bhv9lG+pG9vQ4eD5PVQUvxdANmJpYuDg45nrTmsMC5AHGdFGkHW/LHnkmbFid1cvPYkdiBoef5CIEdoly512pxMupVxnJWF1NT","encoding":{"content":["pkcs8","sr25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"name":"hello"}}') as KeyringPair$Json;
const decoded = decodePair('1', base64Decode(json.encoded), json.encoding.type);
const keyring = new Keyring({ ss58Format: 44 });
it('creates a pair from a private/public combo', (): void => {
const pair = keyring.createFromPair(decoded, json.meta, 'sr25519');
expect(pair.address).toEqual('5PjeoaQzCoYbSi42aQRKB3Sx18StCaEAzCbGEEbWbZyfKS3H');
expect(pair.isLocked).toEqual(false);
expect(pair.meta.name).toEqual('hello');
});
it('adds a pair from a private/public combo', (): void => {
keyring.addFromPair(decoded, json.meta, 'sr25519');
const pair = keyring.getPairs()[0];
expect(pair.address).toEqual('5PjeoaQzCoYbSi42aQRKB3Sx18StCaEAzCbGEEbWbZyfKS3H');
expect(pair.isLocked).toEqual(false);
expect(pair.meta.name).toEqual('hello');
});
});
describe('util', (): void => {
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42 });
});
it('can re-encode an address to Polkadot live', (): void => {
expect(
keyring.encodeAddress('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY', 0)
).toEqual('15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5');
});
it('can re-encode an address to keyring default', (): void => {
expect(
keyring.encodeAddress('15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5')
).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
});
});
describe('version 2 JSON', (): void => {
const PAIR = '{"address":"5CczAE5AmGrZ93MeVhha3Ywam7j9dKB7cArnH7gtrXcMFJvu","encoded":"0xee8f236e2ac3217ce689692a4afc612220dc77fddaed0482f8f95136a7c3e034cccfbc495410a6e9b2439904974ed1d207abeca536ff6985ceb78edeeb3dc343e561c184c488101af8811d1331430b4ccf0e96ef507132e5132964e8564232e7100d973c5bee7b231dd0c8ad5273f3501515a422c8d7ed9d20a73c0ed17c98ee4588e54844bb73052dcad81f7a1094613d63c162fec7446c88b1fae70e","encoding":{"content":["pkcs8","sr25519"],"type":"xsalsa20-poly1305","version":"2"},"meta":{"genesisHash":"0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e","name":"json v2","tags":[],"whenCreated":1595243159596}}';
const PASS2 = 'versionTwo';
const PASS3 = 'versionThree';
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 42 });
});
it('can decode from a version 2 JSON file', (): void => {
const pair = keyring.addFromJson(JSON.parse(PAIR) as KeyringPair$Json);
pair.decodePkcs8(PASS2);
const json = pair.toJson(PASS3);
expect(pair.isLocked).toBe(false);
expect(pair.address).toBe('5CczAE5AmGrZ93MeVhha3Ywam7j9dKB7cArnH7gtrXcMFJvu');
expect(json.encoding).toEqual({
content: ['pkcs8', 'sr25519'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
});
pair.decodePkcs8(PASS3);
expect(pair.address).toEqual('5CczAE5AmGrZ93MeVhha3Ywam7j9dKB7cArnH7gtrXcMFJvu');
});
});
describe('version 3 JSON (hex)', (): void => {
const PAIR = '{"address":"FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH","encoded":"0xcd238963070cc4d6806053ee1ac500c7add9c28732bb5d434a332f84a91d9be0008000000100000008000000cf630a1113941b350ddd06697e50399183162e5e9a0e893eafc7f5f4893a223dca5055706b9925b56fdb4304192143843da718e11717daf89cf4f4781f94fb443f61432f782d54280af9eec90bd3069c3cc2d957a42b7c18dc2e9497f623735518e0e49b58f8e4db2c09da3a45dbb935659d015fc94b946cba75b606a6ff7f4e823f6b049e2e6892026b49de02d6dbbd64646fe0933f537d9ea53a70be","encoding":{"content":["pkcs8","sr25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"version3","tags":[],"whenCreated":1595277797639}}';
const PASS3 = 'version3';
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 2 });
});
it('can decode from a version 3 JSON file', (): void => {
const pair = keyring.addFromJson(JSON.parse(PAIR) as KeyringPair$Json);
pair.decodePkcs8(PASS3);
expect(pair.isLocked).toBe(false);
expect(pair.address).toBe('FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH');
});
});
describe('version 3 JSON (base64)', (): void => {
const PAIR = '{"address":"FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH","encoded":"ILjSgYaGvq1zaCz/kx+aqfLaHBjLXz0Qsmr6RnkOVU4AgAAAAQAAAAgAAAB5R2hm5kgXyc0NQYFxvMU4zCdjB+ugs/ibEooqCvuudbaeKn3Ee47NkCqU1ecOJV+eeaVn4W4dRvIpj5kGmQOGsewR+MiQ/B0G9NFh7JXV0qcPlk2QMNW1/mbJrTO4miqL448BSkP7ZOhUV6HFUpMt3B9HwjiRLN8RORcFp0ID/Azs4Jl/xOpXNzbgQGIffWgCIKTxN9N1ku6tdlG4","encoding":{"content":["pkcs8","sr25519"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{"genesisHash":"0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe","name":"version3","tags":[],"whenCreated":1595277797639,"whenEdited":1595278378596}}';
const PASS3 = 'version3';
let keyring: Keyring;
beforeEach((): void => {
keyring = new Keyring({ ss58Format: 2 });
});
it('can decode from a version 3 JSON file', (): void => {
const pair = keyring.addFromJson(JSON.parse(PAIR) as KeyringPair$Json);
pair.decodePkcs8(PASS3);
expect(pair.isLocked).toBe(false);
expect(pair.address).toBe('FLiSDPCcJ6auZUGXALLj6jpahcP6adVFDBUQznPXUQ7yoqH');
});
});
describe('wordlist', (): void => {
it('creates keypair from different wordlists mnemonics', (): void => {
Object.keys(languages).forEach((language) => {
const mnemonic = mnemonicGenerate(12, languages[language as keyof typeof languages]);
const keyring = new Keyring({
type: 'ed25519'
});
expect(keyring.addFromMnemonic(
mnemonic,
{},
'ed25519',
languages[language as keyof typeof languages]
)).toBeDefined();
});
});
it('cannot create from invalid wordlist', (): void => {
const mnemonic = mnemonicGenerate(12, languages.japanese);
const keyring = new Keyring({
type: 'ed25519'
});
expect(() => keyring.addFromMnemonic(
mnemonic,
{},
'ed25519',
languages.english
)).toThrow('Invalid bip39 mnemonic specified');
});
});
});

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import './packageDetect.js';

View File

@@ -0,0 +1,139 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import type { KeypairType } from '@polkadot/util-crypto/types';
import { contextForSpec } from '@quantus/crypto';
import { u8aConcat } from '@polkadot/util';
import { cryptoWaitReady, decodeAddress, dilithiumSizes } from '@polkadot/util-crypto';
import { Keyring } from './index.js';
import { createTestPairs } from './testingPairs.js';
await cryptoWaitReady();
// Addresses `quantus developer create-test-wallets` prints for the dev accounts.
const ALICE = 'qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7';
const BOB = 'qzkYEQv8tQsmniZYdame3Cku18RL5g9bGK9Pdydq5TMPdpE3y';
const CHARLIE = 'qzntBpmqHZF1jxC8KJKpuxcYuHST892jyXBqRctpAxd1WQ9BL';
const MESSAGE = new Uint8Array([1, 2, 3, 4]);
const PASSWORD = 'correct horse';
// Upstream's keyring specs exercised ed25519, sr25519, ecdsa and ethereum pairs.
// Those types, and their primitives, are gone (quantus/common#6). What replaces
// them is what a post-quantum-only keyring has to guarantee instead.
describe('Keyring (post-quantum only)', (): void => {
describe('refuses quantum-unsafe keys, and says why', (): void => {
const unsafe = ['ed25519', 'sr25519', 'ecdsa', 'ethereum'] as unknown as KeypairType[];
for (const type of unsafe) {
it(`at construction: ${type}`, (): void => {
expect(() => new Keyring({ type })).toThrow(/not quantum-safe and cannot be held here/);
});
it(`from a uri: ${type}`, (): void => {
expect(() => new Keyring().createFromUri('//Alice', {}, type)).toThrow(/not quantum-safe/);
});
}
// The entry point that matters most: a backup file the user chose. This is
// the shape of a real polkadot{.js} export of an ed25519 account.
it('from a polkadot{.js} JSON backup', (): void => {
const json = {
address: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
encoded: '0x00',
encoding: { content: ['pkcs8', 'ed25519'], type: ['scrypt', 'xsalsa20-poly1305'], version: '3' },
meta: { name: 'polkadot account' }
};
expect(() => new Keyring().createFromJson(json as never)).toThrow(/ed25519 keys are not quantum-safe and cannot be held here/);
});
it('and still refuses an unknown type as unknown', (): void => {
expect(() => new Keyring({ type: 'dilithium44' as KeypairType })).toThrow(/found 'dilithium44'/);
});
});
it('defaults to ML-DSA-65', (): void => {
expect(new Keyring().type).toEqual('dilithium65');
});
describe('test pairs are the Quantus dev accounts', (): void => {
const pairs = createTestPairs({ ss58Format: 189 });
it('with the addresses the chain endows', (): void => {
expect(pairs.crystal_alice.address).toEqual(ALICE);
expect(pairs.dilithium_bob.address).toEqual(BOB);
expect(pairs.crystal_charlie.address).toEqual(CHARLIE);
expect(pairs.crystal_alice.type).toEqual('dilithium87');
});
it('and a nobody pair of a type the keyring can hold', (): void => {
expect(pairs.nobody.type).toEqual('dilithium65');
});
});
// Upstream decoded the address and passed it as the public key, which for
// Substrate is the same bytes. Here the address is a hash of the key, so every
// watch-only account reported the hash of its own address.
it('adds a watch-only account at the address it was given', (): void => {
const keyring = new Keyring({ ss58Format: 189 });
const watched = keyring.addFromAddress(ALICE, { name: 'watched' });
expect(watched.address).toEqual(ALICE);
expect(watched.addressRaw).toEqual(decodeAddress(ALICE));
expect(keyring.getPair(ALICE).meta.name).toEqual('watched');
});
describe('JSON round trip', (): void => {
for (const type of ['dilithium65', 'dilithium87'] as const) {
it(`${type}: encrypts, restores locked at the right address, and unlocks`, (): void => {
const keyring = new Keyring({ ss58Format: 189 });
const pair = keyring.addFromSeed(new Uint8Array(32).fill(7), { name: 'roundtrip' }, type);
const restored = keyring.createFromJson(pair.toJson(PASSWORD));
expect(restored.isLocked).toBe(true);
expect(restored.address).toEqual(pair.address);
expect(() => restored.decodePkcs8('wrong')).toThrow();
restored.decodePkcs8(PASSWORD);
expect(restored.publicKey).toEqual(pair.publicKey);
expect(restored.sign(MESSAGE, { context: contextForSpec(148) })).toEqual(pair.sign(MESSAGE, { context: contextForSpec(148) }));
});
}
});
describe('verify', (): void => {
const keyring = new Keyring({ ss58Format: 189 });
const alice = keyring.addFromSeed(new Uint8Array(32), {}, 'dilithium87');
const bob = keyring.addFromSeed(new Uint8Array(32).fill(1), {}, 'dilithium87');
const context = contextForSpec(148);
// what pair.sign returns: the chain's wire form, signature ‖ publicKey
const signature = alice.sign(MESSAGE, { context });
const bare = signature.slice(0, dilithiumSizes('dilithium87').signature);
it('accepts the signature ‖ publicKey form and a bare signature', (): void => {
expect(signature).toEqual(u8aConcat(bare, alice.publicKey));
expect(alice.verify(MESSAGE, signature, alice.publicKey, context)).toBe(true);
expect(alice.verify(MESSAGE, bare, alice.publicKey, context)).toBe(true);
});
it('fails under the wrong context, for the wrong signer, or for another message', (): void => {
expect(alice.verify(MESSAGE, signature, alice.publicKey)).toBe(false);
expect(alice.verify(MESSAGE, signature, bob.publicKey, context)).toBe(false);
expect(alice.verify(new Uint8Array([9]), signature, alice.publicKey, context)).toBe(false);
});
});
it('has no derivation and no VRF', (): void => {
const pair = new Keyring().addFromSeed(new Uint8Array(32));
expect(() => pair.derive('//1')).toThrow(/derive from the mnemonic/);
expect(() => pair.vrfSign(MESSAGE)).toThrow(/VRF signing is not available/);
expect(() => pair.vrfVerify(MESSAGE, new Uint8Array(96), pair.publicKey)).toThrow(/VRF verification is not available/);
});
});

View File

@@ -1,22 +1,43 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { EncryptedJsonEncoding, Keypair, KeypairType } from '@polkadot/util-crypto/types';
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 { hexToU8a, isHex } from '@polkadot/util';
import { base64Decode, decodeAddress, dilithiumPairFromMnemonic, dilithiumPairFromSeed, dilithiumPathFromSuri, encodeAddress, keyExtractSuri } from '@polkadot/util-crypto';
import { createPair } from './pair/index.js';
import { DEV_PHRASE } from './defaults.js';
import { Pairs } from './pairs.js';
const PairFromSeed = {
ecdsa: (seed: Uint8Array): Keypair => secp256k1FromSeed(seed),
ed25519: (seed: Uint8Array): Keypair => ed25519FromSeed(seed),
ethereum: (seed: Uint8Array): Keypair => secp256k1FromSeed(seed),
sr25519: (seed: Uint8Array): Keypair => sr25519FromSeed(seed)
};
/**
* Every keypair type this keyring will construct.
*
* Was two separate inline arrays, both of which a widened `KeypairType` silently
* walked past — the union is compile-time and these checks are not. Named once so
* the next change to the union has one place to look, and so the error message
* cannot drift from the check.
*/
const KEYPAIR_TYPES: KeypairType[] = ['dilithium65', 'dilithium87'];
/** Upstream's types, refused by name so the refusal can say why. */
const QUANTUM_UNSAFE = ['ecdsa', 'ed25519', 'ethereum', 'sr25519'];
/**
* Refuse anything that is not a post-quantum keypair type.
*
* A key of upstream's types arriving here (a JSON backup from polkadot{.js}, a
* caller asking for sr25519) is not a bug in this software, and "unknown crypto
* type" would read as one. Say what it is instead. quantus/common#6
*/
function assertKeypairType (type: string | undefined): asserts type is KeypairType {
if (type && QUANTUM_UNSAFE.includes(type)) {
throw new Error(`${type} keys are not quantum-safe and cannot be held here; only ML-DSA (dilithium65, dilithium87) keys are supported`);
} else if (!KEYPAIR_TYPES.includes(type as KeypairType)) {
throw new Error(`Expected a keypair type of one of ${KEYPAIR_TYPES.map((t) => `'${t}'`).join(', ')}, found '${type || 'unknown'}'`);
}
}
function pairToPublic ({ publicKey }: KeyringPair): Uint8Array {
return publicKey;
@@ -48,11 +69,12 @@ export class Keyring implements KeyringInstance {
public decodeAddress = decodeAddress;
constructor (options: KeyringOptions = {}) {
options.type = options.type || 'ed25519';
// ML-DSA-65 is the scheme new Quantus accounts use.
options.type = options.type || 'dilithium65';
if (!['ecdsa', 'ethereum', 'ed25519', 'sr25519'].includes(options.type || 'undefined')) {
throw new Error(`Expected a keyring type of either 'ed25519', 'sr25519', 'ethereum' or 'ecdsa', found '${options.type || 'unknown'}`);
}
// Behind the union type: a JavaScript caller, or one casting, can still pass
// anything, and this fails at construction rather than at use.
assertKeypairType(options.type);
this.#pairs = new Pairs();
this.#ss58 = options.ss58Format;
@@ -74,7 +96,7 @@ export class Keyring implements KeyringInstance {
}
/**
* @description Returns the type of the keyring, ed25519, sr25519 or ecdsa
* @description Returns the type of the keyring: dilithium65 or dilithium87
*/
public get type (): KeypairType {
return this.#type;
@@ -97,9 +119,14 @@ export class Keyring implements KeyringInstance {
* `addPair` to stores in a keyring pair dictionary the public key of the generated pair as a key and the pair as the associated value.
*/
public addFromAddress (address: string | Uint8Array, meta: KeyringPair$Meta = {}, encoded: Uint8Array | null = null, type: KeypairType = this.type, ignoreChecksum?: boolean, encType?: EncryptedJsonEncoding[]): KeyringPair {
const publicKey = this.decodeAddress(address, ignoreChecksum);
assertKeypairType(type);
return this.addPair(createPair({ toSS58: this.encodeAddress, type }, { publicKey, secretKey: new Uint8Array() }, meta, encoded, encType));
// An address is a Poseidon2 hash of an ML-DSA public key, not the key, so it
// is carried as an account id. Upstream passed it as `publicKey`, which for
// these types made every watch-only account report the hash of its address.
const accountId = this.decodeAddress(address, ignoreChecksum);
return this.addPair(createPair({ toSS58: this.encodeAddress, type }, { accountId, publicKey: new Uint8Array(), secretKey: new Uint8Array() }, meta, encoded, encType));
}
/**
@@ -144,7 +171,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)
this.createFromPair(dilithiumPairFromSeed(seed, type), meta, type)
);
}
@@ -152,15 +179,10 @@ export class Keyring implements KeyringInstance {
* @name addFromUri
* @summary Creates an account via an suri
* @description Extracts the phrase, path and password from a SURI format for specifying secret keys `<secret>/<soft-key>//<hard-key>///<password>` (the `///password` may be omitted, and `/<soft-key>` and `//<hard-key>` maybe repeated and mixed). The secret can be a hex string, mnemonic phrase or a string (to be padded)
* @param _suri
* @param meta
* @param type The supported types of pairs. Either 'ed25519' | 'sr25519' | 'ecdsa' | 'ethereum'.
* @param wordlist - Optional custom wordlist for mnemonic.
* @param rounds - Optional: Number of PBKDF2 iterations to run (default: 210000 (when onlyJS = true) or 2048 (when onlyJS = false).
*/
public addFromUri (suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[], rounds?: number): KeyringPair {
*/
public addFromUri (suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[]): KeyringPair {
return this.addPair(
this.createFromUri(suri, meta, type, wordlist, rounds)
this.createFromUri(suri, meta, type, wordlist)
);
}
@@ -180,19 +202,24 @@ export class Keyring implements KeyringInstance {
? [type]
: type;
if (!['ed25519', 'sr25519', 'ecdsa', 'ethereum'].includes(cryptoType)) {
throw new Error(`Unknown crypto type ${cryptoType}`);
}
// The guard that matters most: JSON arrives from a file the user chose, and a
// polkadot{.js} backup of an sr25519 account must be refused with a reason,
// not constructed and not reported as an unknown type.
assertKeypairType(cryptoType);
// Here the address and publicKey are 32 bytes and isomorphic. This is why the address field needs to be the public key for ethereum type pairs
const publicKey = isHex(address)
const raw = isHex(address)
? hexToU8a(address)
: this.decodeAddress(address, ignoreChecksum);
const decoded = isHex(encoded)
? hexToU8a(encoded)
: base64Decode(encoded);
return createPair({ toSS58: this.encodeAddress, type: cryptoType as KeypairType }, { publicKey, secretKey: new Uint8Array() }, meta, decoded, encType);
// The account id is a one-way Poseidon2 hash, and the 1952/2592-byte public
// key is inside `decoded`, encrypted until someone supplies a password. So
// carry the account id as an account id: createPair uses it for the address
// while locked, and checks it against the real public key the moment
// `decodePkcs8` produces one.
return createPair({ toSS58: this.encodeAddress, type: cryptoType }, { accountId: raw, publicKey: new Uint8Array(), secretKey: new Uint8Array() }, meta, decoded, encType);
}
/**
@@ -200,6 +227,8 @@ export class Keyring implements KeyringInstance {
* @summary Creates a pair from an explicit publicKey/secreteKey combination
*/
public createFromPair (pair: Keypair, meta: KeyringPair$Meta = {}, type: KeypairType = this.type): KeyringPair {
assertKeypairType(type);
return createPair({ toSS58: this.encodeAddress, type }, pair, meta, null);
}
@@ -207,57 +236,41 @@ export class Keyring implements KeyringInstance {
* @name createFromUri
* @summary Creates a Keypair from an suri
* @description This creates a pair from the suri, but does not add it to the keyring
*
* @param _suri
* @param meta
* @param type The supported types of pairs. Either 'ed25519' | 'sr25519' | 'ecdsa' | 'ethereum'.
* @param wordlist - Optional custom wordlist for mnemonic.
* @param rounds - Optional: Number of PBKDF2 iterations to run (default: 210000 (when onlyJS = true) or 2048 (when onlyJS = false).
*/
public createFromUri (_suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, wordlist?: string[], rounds?: number): KeyringPair {
// here we only aut-add the dev phrase if we have a hard-derived path
public createFromUri (_suri: string, meta: KeyringPair$Meta = {}, type: KeypairType = this.type, _wordlist?: string[]): KeyringPair {
assertKeypairType(type);
// here we only auto-add the dev phrase if we have a hard-derived path
const suri = _suri.startsWith('//')
? `${DEV_PHRASE}${_suri}`
: _suri;
const { derivePath, password, path, phrase } = keyExtractSuri(suri);
let seed: Uint8Array;
const isPhraseHex = isHex(phrase, 256);
const { derivePath, password, phrase } = keyExtractSuri(suri);
if (isPhraseHex) {
seed = hexToU8a(phrase);
} else {
const parts = phrase.split(' ');
if ([12, 15, 18, 21, 24].includes(parts.length)) {
seed = type === 'ethereum'
? mnemonicToLegacySeed(phrase, '', false, 64, rounds)
: mnemonicToMiniSecret(phrase, password, wordlist, false, rounds);
} else {
if (phrase.length > 32) {
throw new Error('specified phrase is not a valid mnemonic and is invalid as a raw seed at > 32 bytes');
}
seed = stringToU8a(phrase.padEnd(32));
// ML-DSA derives from the mnemonic itself along a hardened BIP44 path, not
// from a seed along a junction chain: lattice keys have no public
// derivability, so there is no soft junction to emulate, and the chain's own
// Pair::derive refuses for the same reason.
if (isHex(phrase, 256)) {
// A raw 32-byte seed goes straight into keygen, which is how the
// dev-genesis accounts are defined. Combining one with a derivation path
// is ambiguous (is the seed the master, or already derived?), so refuse
// rather than pick.
if (derivePath) {
throw new Error('A derivation path cannot be combined with a raw seed for post-quantum pairs');
}
return this.createFromPair(dilithiumPairFromSeed(hexToU8a(phrase), type), meta, type);
}
const derived = type === 'ethereum'
? isPhraseHex
? PairFromSeed[type](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);
return createPair({ toSS58: this.encodeAddress, type }, derived, meta, null);
return this.createFromPair(
dilithiumPairFromMnemonic(phrase, password || '', dilithiumPathFromSuri(type, derivePath), type),
meta,
type
);
}
/**
* @name encodeAddress
* @description Encodes the input into an ss58 representation
*/
public encodeAddress = (address: Uint8Array | string, ss58Format?: number): string => {
return this.type === 'ethereum'
? ethereumEncode(address)
: encodeAddress(address, ss58Format ?? this.#ss58);
return encodeAddress(address, ss58Format ?? this.#ss58);
};
/**

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './index.js';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @polkadot/dev

View File

@@ -1,6 +1,6 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @polkadot/dev
export const packageInfo = { name: '@polkadot/keyring', path: 'auto', type: 'auto', version: '13.5.6' };
export const packageInfo = { name: '@polkadot/keyring', path: 'auto', type: 'auto', version: '14.0.3-quantus.3' };

View File

@@ -1,26 +0,0 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { createTestPairs } from '../testingPairs.js';
const keyring = createTestPairs({ type: 'ed25519' }, false);
describe('decode', (): void => {
it('fails when no data provided', (): void => {
expect(
(): void => keyring.alice.decodePkcs8()
).toThrow(/No encrypted data available/);
});
it('returns correct publicKey from encoded', (): void => {
const PASS = 'testing';
expect(
(): void => keyring.alice.decodePkcs8(
PASS, keyring.alice.encodePkcs8(PASS)
)
).not.toThrow();
});
});

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { EncryptedJsonEncoding } from '@polkadot/util-crypto/types';
@@ -16,8 +16,20 @@ const SEED_OFFSET = PAIR_HDR.length;
* For divisor/headers, don't rely on the magic being static. These will
* change between generations, aka with the long-awaited 4th generation
* of the format. The external decode interface is the only way to use and decode these.
*
* `secretLength` exists for keys that are not 32 or 64 bytes. The body is
* `HDR ‖ secretKey ‖ DIV ‖ publicKey`, and upstream located the divider by
* trying the two lengths every curve scheme uses. An ML-DSA secret is 4032 or
* 4896 bytes, so neither offset matches and the decode throws "Invalid encoding
* divider found in body" — a stored account that cannot be read back.
*
* The caller passes the length rather than this function searching for the
* divider. Searching would work almost always and fail catastrophically when it
* did not: PAIR_DIV is five bytes, so a 4032-byte secret contains a false match
* about once in 270 million keys, and the result would be a silently wrong key
* rather than an error. The caller knows the type, so it knows the length.
**/
export function decodePair (passphrase?: string, encrypted?: Uint8Array | null, _encType?: EncryptedJsonEncoding | EncryptedJsonEncoding[]): { publicKey: Uint8Array; secretKey: Uint8Array } {
export function decodePair (passphrase?: string, encrypted?: Uint8Array | null, _encType?: EncryptedJsonEncoding | EncryptedJsonEncoding[], secretLength?: number): { publicKey: Uint8Array; secretKey: Uint8Array } {
const encType = Array.isArray(_encType) || _encType === undefined
? _encType
: [_encType];
@@ -29,6 +41,23 @@ export function decodePair (passphrase?: string, encrypted?: Uint8Array | null,
throw new Error('Invalid encoding header found in body');
}
// an explicitly-sized secret (ML-DSA); there is one offset and it either
// matches or the blob is not what the caller said it was
if (secretLength) {
const divOffset = SEED_OFFSET + secretLength;
if (!u8aEq(decrypted.subarray(divOffset, divOffset + PAIR_DIV.length), PAIR_DIV)) {
throw new Error('Invalid encoding divider found in body');
}
return {
// the public key is the remainder: its length varies by scheme and the
// body ends here, so there is nothing to read past it
publicKey: decrypted.subarray(divOffset + PAIR_DIV.length),
secretKey: decrypted.subarray(SEED_OFFSET, divOffset)
};
}
// setup for generation 3 format
let secretKey = decrypted.subarray(SEED_OFFSET, SEED_OFFSET + SEC_LENGTH);
let divOffset = SEED_OFFSET + SEC_LENGTH;

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/** public/secret section divider (generation 1-3, will change in 4, don't rely on value) */

View File

@@ -0,0 +1,91 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { contextForSpec, Scheme, sizes } from '@quantus/crypto';
import { dilithiumVerify } from '@polkadot/util-crypto';
import { Keyring } from '../keyring.js';
// crystal_alice, from the chain's dev genesis: a raw 32-byte seed straight into
// ML-DSA-87 keygen, no HD derivation. The expected address is what
// `quantus developer create-test-wallets` prints — an independent implementation,
// not this one.
const ALICE_ADDRESS = 'qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7';
const MESSAGE = new Uint8Array([0x74, 0x68, 0x65, 0x20, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64]);
describe('dilithium pairs', (): void => {
const keyring = new Keyring({ ss58Format: 189, type: 'dilithium87' });
const alice = keyring.addFromSeed(new Uint8Array(32), { name: 'crystal_alice' }, 'dilithium87');
const context = contextForSpec(148);
const s87 = sizes(Scheme.MlDsa87);
const s65 = sizes(Scheme.MlDsa65);
it('derives the address quantus-cli derives', (): void => {
expect(alice.address).toEqual(ALICE_ADDRESS);
});
// The assumption this whole fork exists to break. Everywhere else in this
// package `addressRaw` and `publicKey` are the same 32 bytes.
it('has an addressRaw that is not the publicKey', (): void => {
expect(alice.addressRaw.length).toEqual(32);
expect(alice.publicKey.length).toEqual(s87.publicKey);
});
it('signs to the wire form the runtime reads', (): void => {
const signature = alice.sign(MESSAGE, { context });
expect(signature.length).toEqual(s87.signatureWithPublicKey);
});
// `withType` prepends the signature enum's variant index. For the curve types
// that enum is Substrate's MultiSignature; here it is the runtime's
// DilithiumSignatureScheme, reached by the same mechanism.
it('prepends the chain variant byte with withType', (): void => {
const signed87 = alice.sign(MESSAGE, { context, withType: true });
const k65 = keyring.addFromSeed(new Uint8Array(32).fill(9), {}, 'dilithium65');
const signed65 = k65.sign(MESSAGE, { context, withType: true });
expect(signed87[0]).toEqual(0);
expect(signed87.length).toEqual(s87.signatureWithPublicKey + 1);
expect(signed65[0]).toEqual(1);
expect(signed65.length).toEqual(s65.signatureWithPublicKey + 1);
});
it('verifies against the account id', (): void => {
const signature = alice.sign(MESSAGE, { context });
expect(dilithiumVerify(MESSAGE, signature, alice.addressRaw, 'dilithium87', context)).toEqual(true);
});
// The spec-148 boundary. A signature made under the wrong context is
// cryptographically valid and rejected by the chain, so this is the only place
// it can be caught locally.
it('does not verify under the wrong context', (): void => {
const signature = alice.sign(MESSAGE, { context });
expect(dilithiumVerify(MESSAGE, signature, alice.addressRaw, 'dilithium87', contextForSpec(147))).toEqual(false);
});
// Verification checks the embedded public key hashes to the account being
// verified against, not merely that the signature is valid for *some* key.
it('does not verify another account signature', (): void => {
const bob = keyring.addFromSeed(new Uint8Array(32).fill(1), {}, 'dilithium87');
const signature = bob.sign(MESSAGE, { context });
expect(dilithiumVerify(MESSAGE, signature, alice.addressRaw, 'dilithium87', context)).toEqual(false);
});
it('refuses to sign without a context', (): void => {
expect(() => alice.sign(MESSAGE)).toThrow(/signing context is required/);
});
// There is no ML-DSA VRF, and faking one from an ordinary signature would
// produce output that looks like a VRF proof and cannot be verified as one.
it('refuses to VRF sign', (): void => {
expect(() => alice.vrfSign(MESSAGE)).toThrow(/not available for dilithium87/);
});
});

View File

@@ -0,0 +1,97 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { dilithiumPath } from '@polkadot/util-crypto';
import { Keyring } from '../keyring.js';
// The well-known Substrate development phrase — public by design, so pinning it
// commits no secret. Any account it derives is assumed compromised.
const DEV_PHRASE = 'bottom drive obey lake curtain smoke basket hold race lonely fit walk';
// What `quantus wallet import --mnemonic-file <DEV_PHRASE> --scheme <s>` prints.
// An independent implementation, not this one.
const EXPECT_65 = 'qzq29m9WvneDAeXbtgueKCREtNe1rVVs6bXSMLmjr6shqvwq6';
const EXPECT_87 = 'qzjrYTUnnE5NduTZKxe9dESCMTZg7nTueKM3bwhnkRdD1iYV4';
// crystal_alice — a raw 32-byte seed straight into keygen, no derivation.
const ALICE = 'qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7';
describe('dilithium derivation', (): void => {
const keyring = new Keyring({ ss58Format: 189, type: 'dilithium65' });
it('builds the paths quantus-cli uses', (): void => {
expect(dilithiumPath('dilithium65')).toEqual("m/44'/189189'/0'/0'/1'");
expect(dilithiumPath('dilithium87')).toEqual("m/44'/189189'/0'/0'/0'");
expect(dilithiumPath('dilithium65', 3)).toEqual("m/44'/189189'/3'/0'/1'");
});
// The whole derivation chain at once: BIP39 to a 64-byte seed (not Substrate's
// mnemonicToMiniSecret), the HMAC-SHA512 walk keyed with "Dilithium seed", the
// trailing hardened index carrying the scheme, and the Poseidon2 account hash.
it('derives what quantus-cli derives, both schemes', (): void => {
expect(keyring.createFromUri(DEV_PHRASE, {}, 'dilithium65').address).toEqual(EXPECT_65);
expect(keyring.createFromUri(DEV_PHRASE, {}, 'dilithium87').address).toEqual(EXPECT_87);
});
it('defaults to account index 0', (): void => {
expect(keyring.createFromUri(`${DEV_PHRASE}//0`, {}, 'dilithium65').address).toEqual(EXPECT_65);
});
it('derives distinct accounts per index', (): void => {
const zero = keyring.createFromUri(`${DEV_PHRASE}//0`, {}, 'dilithium65').address;
const one = keyring.createFromUri(`${DEV_PHRASE}//1`, {}, 'dilithium65').address;
expect(zero).not.toEqual(one);
});
it('accepts an explicit hardened path', (): void => {
const byIndex = keyring.createFromUri(`${DEV_PHRASE}//2`, {}, 'dilithium65').address;
const byPath = keyring.createFromUri(`${DEV_PHRASE}//m/44'/189189'/2'/0'/1'`, {}, 'dilithium65').address;
expect(byPath).toEqual(byIndex);
});
// The suri syntax was built for curve junctions, where `/foo` is a soft
// derivation over arbitrary bytes. Neither form exists for ML-DSA, and quietly
// reinterpreting one would hand back an address no other tool derives.
it('refuses soft derivation', (): void => {
expect(() => keyring.createFromUri(`${DEV_PHRASE}/0`, {}, 'dilithium65')).toThrow(/Soft derivation is not possible/);
});
it('refuses a named junction', (): void => {
expect(() => keyring.createFromUri(`${DEV_PHRASE}//Alice`, {}, 'dilithium65')).toThrow(/Unsupported derivation path/);
});
it('refuses an unhardened level in an explicit path', (): void => {
expect(() => keyring.createFromUri(`${DEV_PHRASE}//m/44'/189189'/0'/0'/1`, {}, 'dilithium65')).toThrow(/Unhardened derivation/);
});
// A raw seed is already key material. Whether it is the master or something
// already derived is unanswerable, so combining it with a path is refused
// rather than guessed.
it('takes a raw hex seed underived, and refuses to derive from one', (): void => {
const seed = `0x${'00'.repeat(32)}`;
expect(keyring.createFromUri(seed, {}, 'dilithium87').address).toEqual(ALICE);
expect(() => keyring.createFromUri(`${seed}//1`, {}, 'dilithium87')).toThrow(/cannot be combined with a raw seed/);
});
it('honours a BIP39 passphrase', (): void => {
const plain = keyring.createFromUri(DEV_PHRASE, {}, 'dilithium65').address;
const withPass = keyring.createFromUri(`${DEV_PHRASE}///hunter2`, {}, 'dilithium65').address;
expect(plain).not.toEqual(withPass);
});
// Deriving a child *from a pair* is not merely unimplemented here, it is
// impossible: ML-DSA keys are not derivable from one another, and the Quantus
// tree derives each account from the mnemonic independently.
it('refuses to derive from an existing pair', (): void => {
const pair = keyring.createFromUri(DEV_PHRASE, {}, 'dilithium65');
expect(() => pair.derive('//1')).toThrow(/derive from the mnemonic with createFromUri/);
});
});

View File

@@ -0,0 +1,103 @@
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { contextForSpec } from '@quantus/crypto';
import { dilithiumVerify } from '@polkadot/util-crypto';
import { Keyring } from '../keyring.js';
// The address `quantus developer create-test-wallets` prints for crystal_alice.
const ALICE_ADDRESS = 'qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7';
const PASSWORD = 'not a good password';
const MESSAGE = new Uint8Array([0x6a, 0x73, 0x6f, 0x6e]);
describe('dilithium account JSON', (): void => {
const keyring = new Keyring({ ss58Format: 189, type: 'dilithium87' });
const context = contextForSpec(148);
const backup = (type: 'dilithium65' | 'dilithium87', fill = 0) => {
const pair = keyring.addFromSeed(new Uint8Array(32).fill(fill), { name: 'backed up' }, type);
return { json: pair.toJson(PASSWORD), original: pair };
};
// The whole point. Before this worked, an exported Quantus account could not be
// imported by anything — decodePair looked for its divider at one of two fixed
// offsets and an ML-DSA secret is at neither, so restore threw "Invalid
// encoding divider found in body". That is a data-durability bug, not a
// convenience one: it had to land before any build let a user create an account.
it('round-trips an ML-DSA-87 account', (): void => {
const { json, original } = backup('dilithium87');
const restored = keyring.createFromJson(json);
restored.decodePkcs8(PASSWORD);
expect(restored.address).toEqual(original.address);
expect(restored.publicKey).toEqual(original.publicKey);
expect(restored.isLocked).toEqual(false);
});
it('round-trips an ML-DSA-65 account', (): void => {
const { json, original } = backup('dilithium65', 5);
const restored = keyring.createFromJson(json);
restored.decodePkcs8(PASSWORD);
expect(restored.address).toEqual(original.address);
expect(restored.publicKey).toEqual(original.publicKey);
});
// A restored pair is locked, and callers read `pair.address` off it long before
// any password appears. For every other scheme that works because the address
// *is* the public key; here the key is still encrypted, so the account id has
// to be carried as data until decodePkcs8 supplies the real one.
it('reports the same address before and after unlock', (): void => {
const { json } = backup('dilithium87');
const restored = keyring.createFromJson(json);
expect(restored.isLocked).toEqual(true);
expect(restored.address).toEqual(ALICE_ADDRESS);
restored.decodePkcs8(PASSWORD);
expect(restored.address).toEqual(ALICE_ADDRESS);
});
it('restores a pair that can still sign', (): void => {
const { json, original } = backup('dilithium87');
const restored = keyring.createFromJson(json);
restored.decodePkcs8(PASSWORD);
const signature = restored.sign(MESSAGE, { context });
expect(dilithiumVerify(MESSAGE, signature, original.addressRaw, 'dilithium87', context)).toEqual(true);
});
it('fails cleanly on a wrong password', (): void => {
const { json } = backup('dilithium87');
const restored = keyring.createFromJson(json);
expect(() => restored.decodePkcs8('wrong')).toThrow();
// and must not have half-applied anything
expect(restored.isLocked).toEqual(true);
expect(restored.address).toEqual(ALICE_ADDRESS);
});
// For every other scheme a tampered `address` field cannot decode at all,
// because the address is the public key. Here it decodes perfectly and yields a
// pair reporting an address its key does not control — a user would see someone
// else's address in their own wallet and believe they held it.
it('rejects JSON whose address does not match its key', (): void => {
const { json } = backup('dilithium87');
const other = keyring.addFromSeed(new Uint8Array(32).fill(2), {}, 'dilithium87');
const tampered = { ...json, address: other.address };
const restored = keyring.createFromJson(tampered);
expect(restored.address).toEqual(other.address);
expect(() => restored.decodePkcs8(PASSWORD)).toThrow(/does not match the address/);
});
});

View File

@@ -1,28 +0,0 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { NONCE_LENGTH, SCRYPT_LENGTH } from '@polkadot/util-crypto/json/constants';
import { createTestPairs } from '../testingPairs.js';
import { PAIR_DIV, PAIR_HDR, PUB_LENGTH, SEC_LENGTH } from './defaults.js';
const DECODED_LENGTH = PAIR_DIV.length + PAIR_HDR.length + PUB_LENGTH + SEC_LENGTH;
const ENCODED_LENGTH = 16 + DECODED_LENGTH + NONCE_LENGTH + SCRYPT_LENGTH;
const keyring = createTestPairs({ type: 'ed25519' }, false);
describe('encode', (): void => {
it('returns PKCS8 when no passphrase supplied', (): void => {
expect(
keyring.alice.encodePkcs8()
).toHaveLength(DECODED_LENGTH);
});
it('returns encoded PKCS8 when passphrase supplied', (): void => {
expect(
keyring.alice.encodePkcs8('testing')
).toHaveLength(ENCODED_LENGTH);
});
});

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { PairInfo } from './types.js';

View File

@@ -1,189 +0,0 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { hexToU8a, u8aToHex } from '@polkadot/util';
import { cryptoWaitReady, encodeAddress as toSS58, setSS58Format } from '@polkadot/util-crypto';
import { PAIRSSR25519 } from '../testing.js';
import { createTestPairs } from '../testingPairs.js';
import { createPair } from './index.js';
const keyring = createTestPairs({ type: 'ed25519' }, false);
const TEST_ADDRESS = '0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887';
await cryptoWaitReady();
describe('pair', (): void => {
const SIGNATURE = new Uint8Array([80, 191, 198, 147, 225, 207, 75, 88, 126, 39, 129, 109, 191, 38, 72, 181, 75, 254, 81, 143, 244, 79, 237, 38, 236, 141, 28, 252, 134, 26, 169, 234, 79, 33, 153, 158, 151, 34, 175, 188, 235, 20, 35, 135, 83, 120, 139, 211, 233, 130, 1, 208, 201, 215, 73, 80, 56, 98, 185, 196, 11, 8, 193, 14]);
it('has a publicKey', (): void => {
expect(
keyring.alice.publicKey
).toEqual(
new Uint8Array([209, 114, 167, 76, 218, 76, 134, 89, 18, 195, 43, 160, 168, 10, 87, 174, 105, 171, 174, 65, 14, 92, 203, 89, 222, 232, 78, 47, 68, 50, 219, 79])
);
expect(
keyring.alice.addressRaw
).toEqual(
new Uint8Array([209, 114, 167, 76, 218, 76, 134, 89, 18, 195, 43, 160, 168, 10, 87, 174, 105, 171, 174, 65, 14, 92, 203, 89, 222, 232, 78, 47, 68, 50, 219, 79])
);
});
it('allows signing', (): void => {
expect(
keyring.alice.sign(
new Uint8Array([0x61, 0x62, 0x63, 0x64])
)
).toEqual(SIGNATURE);
});
it('validates a correctly signed message', (): void => {
expect(
keyring.alice.verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
SIGNATURE,
keyring.alice.publicKey
)
).toEqual(true);
});
it('fails a correctly signed message (signer changed)', (): void => {
expect(
keyring.alice.verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64]),
SIGNATURE,
keyring.bob.publicKey
)
).toEqual(false);
});
it('fails a correctly signed message (message changed)', (): void => {
expect(
keyring.alice.verify(
new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]),
SIGNATURE,
keyring.alice.publicKey
)
).toEqual(false);
});
it('allows vrf sign and verify', (): void => {
const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]);
expect(
keyring.alice.vrfVerify(
message,
keyring.alice.vrfSign(message),
keyring.alice.publicKey
)
).toBe(true);
});
it('fails vrf sign and verify (publicKey changed)', (): void => {
const message = new Uint8Array([0x61, 0x62, 0x63, 0x64, 0x65]);
expect(
keyring.alice.vrfVerify(
message,
keyring.alice.vrfSign(message),
keyring.bob.publicKey
)
).toBe(false);
});
it('allows setting/getting of meta', (): void => {
keyring.bob.setMeta({ foo: 'bar', something: 'else' });
expect(keyring.bob.meta).toMatchObject({ foo: 'bar', something: 'else' });
keyring.bob.setMeta({ something: 'thing' });
expect(keyring.bob.meta).toMatchObject({ foo: 'bar', something: 'thing' });
});
it('allows encoding of address with different prefixes', (): void => {
expect(keyring.alice.address).toEqual('5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua');
// eslint-disable-next-line deprecation/deprecation
setSS58Format(255);
expect(keyring.alice.address).toEqual('yGHU8YKprxHbHdEv7oUK4rzMZXtsdhcXVG2CAMyC9WhzhjH2k');
// eslint-disable-next-line deprecation/deprecation
setSS58Format(42);
});
it('allows getting public key after decoding', (): void => {
const PASS = 'testing';
const encoded = keyring.alice.encodePkcs8(PASS);
const pair = createPair({ toSS58, type: 'sr25519' }, { publicKey: keyring.alice.publicKey });
pair.decodePkcs8(PASS, encoded);
expect(pair.isLocked).toEqual(false);
});
it('allows derivation on the pair', (): void => {
const alice = createPair({ toSS58, type: 'sr25519' }, { publicKey: hexToU8a(PAIRSSR25519[0].p), secretKey: hexToU8a(PAIRSSR25519[0].s) }, {});
const stash = alice.derive('//stash');
const soft = alice.derive('//funding/0');
expect(stash.publicKey).toEqual(hexToU8a(PAIRSSR25519[1].p));
expect(soft.address).toEqual('5ECQNn7UueWHPFda5qUi4fTmTtyCnPvGnuoyVVSj5CboJh9J');
});
it('fails to sign when locked', (): void => {
const pair = createPair({ toSS58, type: 'sr25519' }, { publicKey: keyring.alice.publicKey });
expect(pair.isLocked).toEqual(true);
expect((): Uint8Array =>
pair.sign(new Uint8Array([0]))
).toThrow('Cannot sign with a locked key pair');
});
describe('ethereum', (): void => {
const PUBLICDERIVED = new Uint8Array([
3, 129, 53, 27, 27, 70, 210, 96,
43, 9, 146, 187, 93, 85, 49, 249,
193, 105, 107, 8, 18, 254, 178, 83,
75, 104, 132, 173, 196, 126, 46, 29,
139
]);
const SECRETDERIVED = new Uint8Array([
7, 13, 195, 17, 115, 0, 1, 25,
24, 226, 107, 2, 23, 105, 69, 204,
21, 195, 213, 72, 207, 73, 253, 132,
24, 217, 127, 147, 175, 105, 158, 70
]);
it('has a valid address from a known public', (): void => {
const pair = createPair({ toSS58, type: 'ethereum' }, { publicKey: hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077') });
expect(pair.address).toEqual(TEST_ADDRESS);
expect(pair.addressRaw).toEqual(hexToU8a(TEST_ADDRESS));
});
it('has a valid address from a known ethereum address (20 length)', (): void => {
const pair = createPair({ toSS58, type: 'ethereum' }, { publicKey: new Uint8Array([75, 32, 205, 127, 248, 119, 52, 31, 46, 171, 170, 23, 158, 23, 46, 108, 95, 180, 186, 168]), secretKey: new Uint8Array([]) });
expect(pair.address.toLowerCase()).toEqual('0x4b20cd7ff877341f2eabaa179e172e6c5fb4baa8');
expect(pair.addressRaw).toEqual(hexToU8a('0x4b20cd7ff877341f2eabaa179e172e6c5fb4baa8'));
});
it('converts to json', (): void => {
const pair = createPair({ toSS58, type: 'ethereum' }, { publicKey: PUBLICDERIVED, secretKey: SECRETDERIVED });
const json = pair.toJson('password');
expect(json.encoding).toEqual({
content: ['pkcs8', 'ethereum'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
});
expect(json.address).toEqual(u8aToHex(PUBLICDERIVED));
});
});
});

View File

@@ -1,12 +1,12 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { EncryptedJsonEncoding, Keypair, KeypairType } from '@polkadot/util-crypto/types';
import type { EncryptedJsonEncoding, KeypairType } from '@polkadot/util-crypto/types';
import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta, SignOptions } from '../types.js';
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 { objectSpread, u8aConcat, u8aEmpty, u8aEq, u8aToU8a } from '@polkadot/util';
import { dilithiumAccountFromPublic, dilithiumSign, dilithiumSizes, dilithiumVerify } from '@polkadot/util-crypto';
import { decodePair } from './decode.js';
import { encodePair } from './encode.js';
@@ -19,42 +19,38 @@ interface Setup {
const SIG_TYPE_NONE = new Uint8Array();
const TYPE_FROM_SEED = {
ecdsa: secp256k1FromSeed,
ed25519: ed25519FromSeed,
ethereum: secp256k1FromSeed,
sr25519: sr25519FromSeed
// The Quantus runtime's `DilithiumSignatureScheme`: `Dilithium87` is variant 0
// and `Dilithium65` variant 1. Reached through the same `withType` mechanism
// upstream used for Substrate's `MultiSignature`, so nothing above this file
// needs to know which enum it is.
//
// There are no other arms. Upstream's ed25519, sr25519, ecdsa and ethereum were
// removed with their primitives: each falls to Shor's algorithm, and this keyring
// exists to hold keys that do not. quantus/common#6
const TYPE_PREFIX: Record<KeypairType, Uint8Array> = {
dilithium65: new Uint8Array([1]),
dilithium87: new Uint8Array([0])
};
const TYPE_PREFIX = {
ecdsa: new Uint8Array([2]),
ed25519: new Uint8Array([0]),
ethereum: new Uint8Array([2]),
sr25519: new Uint8Array([1])
};
/**
* A post-quantum pair will not sign without being told which context to use.
*
* See `SignOptions.context`. The alternative — defaulting to the extrinsic
* context — produces a valid signature that a spec-147 chain rejects, with
* nothing local able to explain why.
*/
function requireContext (context?: Uint8Array): Uint8Array {
if (!context) {
throw new Error('A signing context is required for post-quantum keypairs; pass options.context (see contextForSpec)');
}
const TYPE_SIGNATURE = {
ecdsa: (m: Uint8Array, p: Partial<Keypair>) => secp256k1Sign(m, p, 'blake2'),
ed25519: ed25519Sign,
ethereum: (m: Uint8Array, p: Partial<Keypair>) => secp256k1Sign(m, p, 'keccak'),
sr25519: sr25519Sign
};
const TYPE_ADDRESS = {
ecdsa: (p: Uint8Array) => p.length > 32 ? blake2AsU8a(p) : p,
ed25519: (p: Uint8Array) => p,
ethereum: (p: Uint8Array) => p.length === 20 ? p : keccakAsU8a(secp256k1Expand(p)),
sr25519: (p: Uint8Array) => p
};
return context;
}
function isLocked (secretKey?: Uint8Array): secretKey is undefined {
return !secretKey || u8aEmpty(secretKey);
}
function vrfHash (proof: Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): Uint8Array {
return blake2AsU8a(u8aConcat(context || '', extra || '', proof));
}
/**
* @name createPair
* @summary Creates a keyring pair object
@@ -86,18 +82,29 @@ function vrfHash (proof: Uint8Array, context?: string | Uint8Array, extra?: stri
* an `encoded` property that is assigned with the encoded public key in hex format, and an `encoding`
* property that indicates whether the public key value of the `encoded` property is encoded or not.
*/
export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: PairInfo, meta: KeyringPair$Meta = {}, encoded: Uint8Array | null = null, encTypes?: EncryptedJsonEncoding[]): KeyringPair {
export function createPair ({ toSS58, type }: Setup, { accountId, publicKey, secretKey }: PairInfo, meta: KeyringPair$Meta = {}, encoded: Uint8Array | null = null, encTypes?: EncryptedJsonEncoding[]): KeyringPair {
const decodePkcs8 = (passphrase?: string, userEncoded?: Uint8Array | null): void => {
const decoded = decodePair(passphrase, userEncoded || encoded, encTypes);
// ML-DSA secrets are 4032/4896 bytes, which is neither of the two lengths
// decodePair would otherwise try, so it has to be told.
const decoded = decodePair(passphrase, userEncoded || encoded, encTypes, dilithiumSizes(type).secretKey);
if (decoded.secretKey.length === 64) {
publicKey = decoded.publicKey;
secretKey = decoded.secretKey;
} else {
const pair = TYPE_FROM_SEED[type](decoded.secretKey);
publicKey = decoded.publicKey;
secretKey = decoded.secretKey;
publicKey = pair.publicKey;
secretKey = pair.secretKey;
// The public key has only now arrived. If the pair was constructed from JSON
// it has been reporting an address carried as data since then, so check the
// two agree.
//
// This is not a paranoid check. A JSON file whose `address` field has been
// edited decodes perfectly and would yield a pair reporting an address its
// key does not control: a user would see an attacker's address in their own
// wallet and believe they held it.
if (accountId) {
if (!u8aEq(dilithiumAccountFromPublic(publicKey), accountId)) {
throw new Error('Decoded public key does not match the address in the account JSON');
}
accountId = undefined;
}
};
@@ -110,24 +117,19 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P
return encoded;
};
const encodeAddress = (): string => {
const raw = TYPE_ADDRESS[type](publicKey);
return type === 'ethereum'
? ethereumEncode(raw)
: toSS58(raw);
};
// While a pair restored from JSON, or added by address, has no public key,
// its account id cannot be computed (it is a one-way Poseidon2 hash of that
// key), so use the one carried alongside. `decodePkcs8` clears it once the
// real key arrives and has been checked against it.
const addressRawOf = (): Uint8Array =>
accountId ?? dilithiumAccountFromPublic(publicKey);
return {
get address (): string {
return encodeAddress();
return toSS58(addressRawOf());
},
get addressRaw (): Uint8Array {
const raw = TYPE_ADDRESS[type](publicKey);
return type === 'ethereum'
? raw.slice(-20)
: raw;
return addressRawOf();
},
get isLocked (): boolean {
return isLocked(secretKey);
@@ -143,17 +145,12 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P
},
// eslint-disable-next-line sort-keys
decodePkcs8,
derive: (suri: string, meta?: KeyringPair$Meta): KeyringPair => {
if (type === 'ethereum') {
throw new Error('Unable to derive on this keypair');
} else if (isLocked(secretKey)) {
throw new Error('Cannot derive on a locked keypair');
}
const { path } = keyExtractPath(suri);
const derived = keyFromPath({ publicKey, secretKey }, path, type);
return createPair({ toSS58, type }, derived, meta, null);
derive: (_suri: string, _meta?: KeyringPair$Meta): KeyringPair => {
// Not "not implemented" — not possible. A child here would have to come
// from this pair's key material, and ML-DSA keys are not derivable from
// one another at all; the Quantus tree derives every account from the
// mnemonic independently. So the caller needs the mnemonic, not this pair.
throw new Error(`Unable to derive from an existing ${type} pair; derive from the mnemonic with createFromUri instead`);
},
encodePkcs8: (passphrase?: string): Uint8Array => {
return recode(passphrase);
@@ -173,48 +170,37 @@ export function createPair ({ toSS58, type }: Setup, { publicKey, secretKey }: P
options.withType
? TYPE_PREFIX[type]
: SIG_TYPE_NONE,
TYPE_SIGNATURE[type](u8aToU8a(message), { publicKey, secretKey })
dilithiumSign(u8aToU8a(message), { publicKey, secretKey }, type, requireContext(options.context))
);
},
toJson: (passphrase?: string): KeyringPair$Json => {
// NOTE: For ecdsa and ethereum, the publicKey cannot be extracted from the address. For these
// pass the hex-encoded publicKey through to the address portion of the JSON (before decoding)
// unless the publicKey is already an address
const address = ['ecdsa', 'ethereum'].includes(type)
? publicKey.length === 20
? u8aToHex(publicKey)
: u8aToHex(secp256k1Compress(publicKey))
: encodeAddress();
return pairToJson(type, { address, meta }, recode(passphrase), !!passphrase);
return pairToJson(type, { address: toSS58(addressRawOf()), meta }, recode(passphrase), !!passphrase);
},
unlock: (passphrase?: string): void => {
return decodePkcs8(passphrase);
},
verify: (message: string | Uint8Array, signature: string | Uint8Array, signerPublic: string | Uint8Array): boolean => {
return signatureVerify(message, signature, TYPE_ADDRESS[type](u8aToU8a(signerPublic))).isValid;
verify: (message: string | Uint8Array, signature: string | Uint8Array, signerPublic: string | Uint8Array, context: Uint8Array = new Uint8Array()): boolean => {
const pk = u8aToU8a(signerPublic);
const sig = u8aToU8a(signature);
const { signature: sigLength } = dilithiumSizes(type);
// Accept a bare signature or the chain's `signature ‖ publicKey` form; the
// verifier needs the latter, and the public key is at hand either way.
const sigWithPublic = sig.length === sigLength
? u8aConcat(sig, pk)
: sig;
// Unlike signing, a wrong context here cannot produce anything harmful —
// only `false` — so the empty context used for raw bytes is the default.
return dilithiumVerify(u8aToU8a(message), sigWithPublic, dilithiumAccountFromPublic(pk), type, context);
},
vrfSign: (message: string | Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): Uint8Array => {
if (isLocked(secretKey)) {
throw new Error('Cannot sign with a locked key pair');
}
if (type === 'sr25519') {
return sr25519VrfSign(message, { secretKey }, context, extra);
}
const proof = TYPE_SIGNATURE[type](u8aToU8a(message), { publicKey, secretKey });
return u8aConcat(vrfHash(proof, context, extra), proof);
vrfSign: (): Uint8Array => {
// There is no ML-DSA VRF, and faking one out of an ordinary signature needs
// uniqueness ML-DSA does not have in the form required.
throw new Error(`VRF signing is not available for ${type}`);
},
vrfVerify: (message: string | Uint8Array, vrfResult: Uint8Array, signerPublic: Uint8Array | string, context?: string | Uint8Array, extra?: string | Uint8Array): boolean => {
if (type === 'sr25519') {
return sr25519VrfVerify(message, vrfResult, publicKey, context, extra);
}
const result = signatureVerify(message, u8aConcat(TYPE_PREFIX[type], vrfResult.subarray(32)), TYPE_ADDRESS[type](u8aToU8a(signerPublic)));
return result.isValid && u8aEq(vrfResult.subarray(0, 32), vrfHash(vrfResult.subarray(32), context, extra));
vrfVerify: (): boolean => {
throw new Error(`VRF verification is not available for ${type}`);
}
};
}

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyringPair, KeyringPair$Json, KeyringPair$Meta } from '../types.js';
@@ -18,7 +18,7 @@ const json: KeyringPair$Json = {
address,
encoded: '',
encoding: {
content: ['pkcs8', 'ed25519'],
content: ['pkcs8', 'dilithium65'],
type: 'none',
version: '0'
},
@@ -46,7 +46,7 @@ const pair: KeyringPair = {
new Uint8Array(64),
toJson: (_passphrase?: string): KeyringPair$Json =>
json,
type: 'ed25519',
type: 'dilithium65',
unlock: (_passphrase?: string): void =>
undefined,
verify: (_message: Uint8Array, _signature: Uint8Array): boolean =>

View File

@@ -1,42 +0,0 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { createTestPairs } from '../testingPairs.js';
const keyring = createTestPairs({ type: 'ed25519' }, false);
describe('toJson', (): void => {
it('creates an unencoded output with no passphrase', (): void => {
expect(
keyring.alice.toJson()
).toMatchObject({
address: '5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua',
encoded: 'MFMCAQEwBQYDK2VwBCIEIEFsaWNlICAgICAgICAgICAgICAgICAgICAgICAgICAg0XKnTNpMhlkSwyugqApXrmmrrkEOXMtZ3uhOL0Qy20+hIwMhANFyp0zaTIZZEsMroKgKV65pq65BDlzLWd7oTi9EMttP',
encoding: {
content: ['pkcs8', 'ed25519'],
type: ['none'],
version: '3'
},
meta: {
isTesting: true,
name: 'alice'
}
});
});
it('creates an encoded output with passphrase', (): void => {
const json = keyring.alice.toJson('testing');
expect(json.encoded).toHaveLength(268);
expect(json).toMatchObject({
address: '5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua',
encoding: {
content: ['pkcs8', 'ed25519'],
type: ['scrypt', 'xsalsa20-poly1305'],
version: '3'
}
});
});
});

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeypairType } from '@polkadot/util-crypto/types';

View File

@@ -1,7 +1,22 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
export interface PairInfo {
/**
* The 32-byte account id, when it cannot be derived from `publicKey`.
*
* Only ML-DSA pairs need this, and only while locked. Everywhere else the
* address is a cheap function of the public key, so a pair restored from JSON
* can report its address before anyone supplies a password. A Quantus account
* id is a one-way Poseidon2 hash, and the public key lives inside the
* encrypted blob — so between `createFromJson` and `decodePkcs8` there is
* nothing to compute it from, and callers read `pair.address` in exactly that
* window.
*
* Carried as data rather than derived, and checked against the real public key
* once that arrives.
*/
accountId?: Uint8Array | undefined;
publicKey: Uint8Array;
secretKey?: Uint8Array | undefined;
seed?: Uint8Array | null;

View File

@@ -1,47 +0,0 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { u8aToHex } from '@polkadot/util';
import { cryptoWaitReady, ed25519PairFromSeed, encodeAddress as toSS58, randomAsU8a, secp256k1PairFromSeed, sr25519PairFromSeed } from '@polkadot/util-crypto';
import { createPair } from './index.js';
const MESSAGE = 'this is a test message';
const CONTEXT = 'some context';
await cryptoWaitReady();
const ecdsa = createPair({ toSS58, type: 'ecdsa' }, secp256k1PairFromSeed(randomAsU8a()));
const ed25519 = createPair({ toSS58, type: 'ed25519' }, ed25519PairFromSeed(randomAsU8a()));
const sr25519 = createPair({ toSS58, type: 'sr25519' }, sr25519PairFromSeed(randomAsU8a()));
describe('vrf', (): void => {
it('has deterministic signature values for ecdsa', (): void => {
const sig1 = ecdsa.vrfSign(MESSAGE, CONTEXT);
const sig2 = ecdsa.vrfSign(MESSAGE, CONTEXT);
expect(u8aToHex(sig1)).toEqual(u8aToHex(sig2));
expect(ecdsa.vrfVerify(MESSAGE, sig1, ecdsa.publicKey, CONTEXT)).toEqual(true);
expect(ecdsa.vrfVerify(MESSAGE, sig2, ecdsa.publicKey, CONTEXT)).toEqual(true);
});
it('has deterministic signature values for ed25519', (): void => {
const sig1 = ed25519.vrfSign(MESSAGE, CONTEXT);
const sig2 = ed25519.vrfSign(MESSAGE, CONTEXT);
expect(u8aToHex(sig1)).toEqual(u8aToHex(sig2));
expect(ed25519.vrfVerify(MESSAGE, sig1, ed25519.publicKey, CONTEXT)).toEqual(true);
expect(ed25519.vrfVerify(MESSAGE, sig2, ed25519.publicKey, CONTEXT)).toEqual(true);
});
it('has deterministic signature values for sr25519', (): void => {
const sig1 = sr25519.vrfSign(MESSAGE, CONTEXT);
const sig2 = sr25519.vrfSign(MESSAGE, CONTEXT);
expect(u8aToHex(sig1.slice(0, 32))).toEqual(u8aToHex(sig2.slice(0, 32)));
expect(sr25519.vrfVerify(MESSAGE, sig1, sr25519.publicKey, CONTEXT)).toEqual(true);
expect(sr25519.vrfVerify(MESSAGE, sig2, sr25519.publicKey, CONTEXT)).toEqual(true);
});
});

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyringPair, KeyringPairs } from './types.js';

View File

@@ -1,109 +0,0 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
// From https://github.com/paritytech/substrate/wiki/Secret-URI-Test-Vectors
import type { KeypairType } from '@polkadot/util-crypto/types';
import { u8aToHex } from '@polkadot/util';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import Keyring from './index.js';
const PHRASE = 'bottom drive obey lake curtain smoke basket hold race lonely fit walk';
const ETHEREUM_PHRASE = 'seed sock milk update focus rotate barely fade car face mechanic mercy';
const TESTS = {
ecdsa: [
{
pk: '0x020a1091341fe5664bfa1782d5e04779689068c916b04cb365ec3153755684d9a1',
ss: '5C7C2Z5sWbytvHpuLTvzKunnnRwQxft1jiqrLD5rhucQ5S9X',
uri: `${PHRASE}//Alice`
}
],
ethereum: [
{
pk: '0x0381351b1b46d2602b0992bb5d5531f9c1696b0812feb2534b6884adc47e2e1d8b',
ss: '0x31ea8795EE32D782C8ff41a5C68Dcbf0F5B27f6d',
uri: `${ETHEREUM_PHRASE}/m/44'/60'/0'/0/0`
},
{
pk: '0x02509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9f',
ss: '0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac',
uri: `${PHRASE}/m/44'/60'/0'/0/0`
},
{
pk: '0x033bc19e36ff1673910575b6727a974a9abd80c9a875d41ab3e2648dbfb9e4b518',
ss: '0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0',
uri: `${PHRASE}/m/44'/60'/0'/0/1`
}
],
sr25519: [
{
pk: '0x46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a',
ss: '5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV',
uri: PHRASE
},
{
pk: '0xb69355deefa7a8f33e9297f5af22e680f03597a99d4f4b1c44be47e7a2275802',
ss: '5GC6LfpV352HtJPySfAecb5JdePtf4R9Vq49NUU8RhzgBqgq',
uri: `${PHRASE}///password`
},
{
pk: '0x40b9675df90efa6069ff623b0fdfcf706cd47ca7452a5056c7ad58194d23440a',
ss: '5DXZzrDxHbkQov4QBAY4TjpwnHCMrKXkomTnKSw8UArBEY5v',
uri: `${PHRASE}/foo`
},
{
pk: '0x547d4a55642ec7ebadc0bd29b6e570b8c926059b3c0655d4948075e9a7e6f31e',
ss: '5DyV6fZuvPemWrUqBgWwTSgoV86w6xms3KhkFU6cQcWxU8eP',
uri: `${PHRASE}//foo`
},
{
pk: '0x3841947ffcde6f5fef26fb68b59bb8665637e30e32ec2051f99cf6b9c674fe09',
ss: '5DLU27is5iViNopQb2KxsTyPx6j4vCu8X3sk3j3NNLkPCqKM',
uri: `${PHRASE}//foo/bar`
},
{
pk: '0xdc142f7476a7b0aa262aeccf207f1d18daa90762db393006741e8a31f39dbc53',
ss: '5H3GPTqDSpjkfDwbHy12PD6BWm8jvGSX4xYC8UMprHpTPcRg',
uri: `${PHRASE}/foo//bar`
},
{
pk: '0xa2e56b06407a6d1e819d2fc33fa0ec604b29c2e868b70b3696bb049b8725934b',
ss: '5FkHmNgbg64MwStgCyDi2Uw3ufFu11mqQgmWT9uwK4Lghvpv',
uri: `${PHRASE}//foo/bar//42/69`
},
{
pk: '0x0e0d24e3e1ff2c07f269c99e2e0df8681fda1851ac42fc846ca2daaa90cd8f14',
ss: '5CP8S23JBNXYNpJsL7ESPJBNnUZE6itcfM4EnDxEhaVEU6dT',
uri: `${PHRASE}//foo/bar//42/69///password`
},
{
pk: '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d',
ss: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
uri: `${PHRASE}//Alice`
}
]
};
await cryptoWaitReady();
describe('keyring.addFromUri', (): void => {
for (const [type, tests] of Object.entries(TESTS)) {
const keyring = new Keyring({ type: type as KeypairType });
describe(`${type}`, (): void => {
tests.forEach(({ pk, ss, uri }): void => {
it(`creates ${uri}`, (): void => {
const pair = keyring.addFromUri(uri, {}, type as KeypairType, undefined, 2048);
expect(u8aToHex(pair.publicKey)).toEqual(pk);
expect(pair.address).toEqual(ss);
});
});
});
}
});

View File

@@ -1,151 +1,48 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@polkadot/util/types';
import type { KeypairType } from '@polkadot/util-crypto/types';
import type { KeyringInstance, KeyringOptions } from './types.js';
import { hexToU8a } from '@polkadot/util';
import { createPair } from './pair/index.js';
import { Keyring } from './keyring.js';
interface PairDef {
name?: string;
p: HexString;
s: HexString;
seed?: string;
type: KeypairType
}
// NOTE This is not great since we have the secretKey here explicitly, but a testing
// keyring is for testing - what happens is that in most cases the keyring is initialises
// before anything else. Since the sr25519 crypto is async, this creates problems with
// adding the keys when only the keyring is used.
export const PAIRSSR25519: PairDef[] = [
{
p: '0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d',
s: '0x98319d4ff8a9508c4bb0cf0b5a78d760a0b2082c02775e6e82370816fedfff48925a225d97aa00682d6a59b95b18780c10d7032336e88f3442b42361f4a66011', // nosemgrep
seed: 'Alice',
type: 'sr25519'
},
{
p: '0xbe5ddb1579b72e84524fc29e78609e3caf42e85aa118ebfe0b0ad404b5bdd25f',
s: '0xe8da6c9d810e020f5e3c7f5af2dea314cbeaa0d72bc6421e92c0808a0c584a6046ab28e97c3ffc77fe12b5a4d37e8cd4afbfebbf2391ffc7cb07c0f38c023efd', // nosemgrep
seed: 'Alice//stash',
type: 'sr25519'
},
{
p: '0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48',
s: '0x081ff694633e255136bdb456c20a5fc8fed21f8b964c11bb17ff534ce80ebd5941ae88f85d0c1bfc37be41c904e1dfc01de8c8067b0d6d5df25dd1ac0894a325', // nosemgrep
seed: 'Bob',
type: 'sr25519'
},
{
p: '0xfe65717dad0447d715f660a0a58411de509b42e6efb8375f562f58a554d5860e',
s: '0xc006507cdfc267a21532394c49ca9b754ca71de21e15a1cdf807c7ceab6d0b6c3ed408d9d35311540dcd54931933e67cf1ea10d46f75408f82b789d9bd212fde', // nosemgrep
seed: 'Bob//stash',
type: 'sr25519'
},
{
p: '0x90b5ab205c6974c9ea841be688864633dc9ca8a357843eeacf2314649965fe22',
s: '0xa8f2d83016052e5d6d77b2f6fd5d59418922a09024cda701b3c34369ec43a7668faf12ff39cd4e5d92bb773972f41a7a5279ebc2ed92264bed8f47d344f8f18c', // nosemgrep
seed: 'Charlie',
type: 'sr25519'
},
{
p: '0x306721211d5404bd9da88e0204360a1a9ab8b87c66c1bc2fcdd37f3c2222cc20',
s: '0x20e05482ca4677e0edbc58ae9a3a59f6ed3b1a9484ba17e64d6fe8688b2b7b5d108c4487b9323b98b11fe36cb301b084e920f7b7895536809a6d62a451b25568', // nosemgrep
seed: 'Dave',
type: 'sr25519'
},
{
p: '0xe659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df4e',
s: '0x683576abfd5dc35273e4264c23095a1bf21c14517bece57c7f0cc5c0ed4ce06a3dbf386b7828f348abe15d76973a72009e6ef86a5c91db2990cb36bb657c6587', // nosemgrep
seed: 'Eve',
type: 'sr25519'
},
{
p: '0x1cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c',
s: '0xb835c20f450079cf4f513900ae9faf8df06ad86c681884122c752a4b2bf74d4303e4f21bc6cc62bb4eeed5a9cce642c25e2d2ac1464093b50f6196d78e3a7426', // nosemgrep
seed: 'Ferdie',
type: 'sr25519'
}
];
export const PAIRSETHEREUM: PairDef[] = [
{
name: 'Alith',
p: '0x02509540919faacf9ab52146c9aa40db68172d83777250b28e4679176e49ccdd9f',
s: '0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133', // nosemgrep
type: 'ethereum'
},
{
name: 'Baltathar',
p: '0x033bc19e36ff1673910575b6727a974a9abd80c9a875d41ab3e2648dbfb9e4b518',
s: '0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b', // nosemgrep
type: 'ethereum'
},
{
name: 'Charleth',
p: '0x0234637bdc0e89b5d46543bcbf8edff329d2702bc995e27e9af4b1ba009a3c2a5e',
s: '0x0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b', // nosemgrep
type: 'ethereum'
},
{
name: 'Dorothy',
p: '0x02a00d60b2b408c2a14c5d70cdd2c205db8985ef737a7e55ad20ea32cc9e7c417c',
s: '0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68', // nosemgrep
type: 'ethereum'
},
{
name: 'Ethan',
p: '0x025cdc005b752651cd3f728fb9192182acb3a9c89e19072cbd5b03f3ee1f1b3ffa',
s: '0x7dce9bc8babb68fec1409be38c8e1a52650206a7ed90ff956ae8a6d15eeaaef4', // nosemgrep
type: 'ethereum'
},
{
name: 'Faith',
p: '0x037964b6c9d546da4646ada28a99e34acaa1d14e7aba861a9055f9bd200c8abf74',
s: '0xb9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df', // nosemgrep
type: 'ethereum'
}
];
function createMeta (name?: string, seed?: string) {
if (!name && !seed) {
throw new Error('Testing pair should have either a name or a seed');
}
return {
isTesting: true,
name: name || seed?.replace('//', '_').toLowerCase()
};
interface DevAccount {
name: string;
/** The byte the 32-byte seed is filled with. */
seed: number;
}
/**
* @name testKeyring
* @summary Create an instance of Keyring pre-populated with locked test accounts
* @description The test accounts (i.e. alice, bob, dave, eve, ferdie)
* are available on the dev chain and each test account is initialized with DOT funds.
* The accounts a Quantus dev chain endows at genesis.
*
* From `chain:primitives/dilithium-crypto/src/pair.rs`: ML-DSA-87 keys generated
* directly from a 32-byte seed filled with one byte, with no HD derivation. Their
* account ids are pinned against `quantus-cli` in `@quantus/crypto`'s tests
* (`dev_account_ids_match_the_cli`), e.g. crystal_alice is
* `qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7`.
*
* They replace upstream's sr25519 Alice…Ferdie and Ethereum Alith…Faith, which
* this keyring can no longer hold. quantus/common#6
*/
export function createTestKeyring (options: KeyringOptions = {}, isDerived = true): KeyringInstance {
const keyring = new Keyring(options);
const pairs = options.type === 'ethereum'
? PAIRSETHEREUM
: PAIRSSR25519;
export const DEV_ACCOUNTS: DevAccount[] = [
{ name: 'crystal_alice', seed: 0 },
{ name: 'dilithium_bob', seed: 1 },
{ name: 'crystal_charlie', seed: 2 }
];
for (const { name, p, s, seed, type } of pairs) {
const meta = createMeta(name, seed);
const pair = !isDerived && !name && seed
? keyring.addFromUri(seed, meta, options.type)
: keyring.addPair(
createPair(
{ toSS58: keyring.encodeAddress, type },
{ publicKey: hexToU8a(p), secretKey: hexToU8a(s) },
meta
)
);
/**
* @name testKeyring
* @summary Create an instance of Keyring pre-populated with the dev accounts
* @description crystal_alice, dilithium_bob and crystal_charlie, as a Quantus
* dev chain endows them. They are always ML-DSA-87, whatever `options.type` says,
* because that is what the chain made them; `options` sets the keyring's default
* type and SS58 format for anything added afterwards. `_isDerived` is accepted for
* upstream's signature and ignored: these accounts have no derivation.
*/
export function createTestKeyring (options: KeyringOptions = {}, _isDerived = true): KeyringInstance {
const keyring = new Keyring(options);
for (const { name, seed } of DEV_ACCOUNTS) {
const pair = keyring.addFromSeed(new Uint8Array(32).fill(seed), { isTesting: true, name }, 'dilithium87');
pair.lock = (): void => {
// we don't have lock/unlock functionality here

View File

@@ -1,79 +0,0 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { u8aToHex } from '@polkadot/util';
import { cryptoWaitReady } from '@polkadot/util-crypto';
import Keyring from './index.js';
import { createTestPairs } from './testingPairs.js';
const TEST_ADD = '0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac';
await cryptoWaitReady();
describe('testingPairs', (): void => {
it('creates without failing', (): void => {
expect(
Object.keys(createTestPairs())
).toHaveLength(2 + 0 + 7); // stash, session, pairs
});
it('has the correct address for Alice (non-HDKD)', (): void => {
expect(
createTestPairs({ type: 'ed25519' }, false).alice.address
).toEqual('5GoKvZWG5ZPYL1WUovuHW3zJBWBP5eT8CbqjdRY4Q6iMaQua');
});
it('has the correct address for Alice (HDKD)', (): void => {
expect(
createTestPairs({ type: 'ed25519' }).alice.address
).toEqual('5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY');
});
it('has the correct address for Alith (Eth)', (): void => {
expect(
createTestPairs({ type: 'ethereum' }).Alith.address
).toEqual(TEST_ADD);
});
it('has the correct address for Alith (Eth), same as obtained by createFromUri', (): void => {
const keyring = new Keyring({ type: 'ethereum' });
const pair = keyring.createFromUri('0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133');
expect(pair?.address).toEqual(TEST_ADD);
});
describe('checks eth test addresses', (): void => {
const ring = createTestPairs({ type: 'ethereum' });
const keyring = new Keyring({ type: 'ethereum' });
// priv keys generated by ganache-cli --mnemonic "bottom drive obey lake curtain smoke basket hold race lonely fit walk"
const privKeys: string[] = ['0x5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133',
'0x8075991ce870b93a8870eca0c0f91913d12f47948ca0fd25b49c6fa7cdbeee8b',
'0x0b6e18cafb6ed99687ec547bd28139cafdd2bffe70e6b688025de6b445aa5c5b',
'0x39539ab1876910bbf3a223d84a29e28f1cb4e2e456503e7e91ed39b2e7223d68',
'0x7dce9bc8babb68fec1409be38c8e1a52650206a7ed90ff956ae8a6d15eeaaef4',
'0xb9d2ea9a615f3165812e8d44de0d24da9bbd164b65c4f0573e1ce2c8dbd9c8df',
'0x96b8a38e12e1a31dee1eab2fffdf9d9990045f5b37e44d8cc27766ef294acf18',
'0x0d6dcaaef49272a5411896be8ad16c01c35d6f8c18873387b71fbc734759b0ab',
'0x4c42532034540267bf568198ccec4cb822a025da542861fcb146a5fab6433ff8',
'0x94c49300a58d576011096bcb006aa06f5a91b34b4383891e8029c21dc39fbb8b'];
// @ts-expect-error We should not delete from the maps, however this is a test
delete ring.nobody;
Object
.keys(ring)
.filter((_, i) => i < 6)
.forEach((testKeyring, i) => {
it(`checks #${i}`, (): void => {
expect(
u8aToHex(ring[testKeyring].publicKey)
).toEqual(
u8aToHex(keyring.createFromUri(privKeys[i]).publicKey)
);
});
});
});
});

View File

@@ -1,7 +1,6 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeypairType } from '@polkadot/util-crypto/types';
import type { KeyringOptions, KeyringPair } from './types.js';
import { nobody } from './pair/nobody.js';
@@ -9,39 +8,17 @@ import { createTestKeyring } from './testing.js';
export interface TestKeyringMap {
nobody: KeyringPair;
[index: string]: KeyringPair;
}
export interface TestKeyringMapSubstrate extends TestKeyringMap {
alice: KeyringPair;
bob: KeyringPair;
charlie: KeyringPair;
dave: KeyringPair;
eve: KeyringPair;
ferdie: KeyringPair;
/** The Quantus dev accounts, by name. See `DEV_ACCOUNTS`. */
export interface TestKeyringMapQuantus extends TestKeyringMap {
crystal_alice: KeyringPair;
crystal_charlie: KeyringPair;
dilithium_bob: KeyringPair;
}
export interface TestKeyringMapEthereum extends TestKeyringMap {
Alith: KeyringPair;
Baltathar: KeyringPair;
Charleth: KeyringPair;
Dorothy: KeyringPair;
Ethan: KeyringPair;
Faith: KeyringPair;
}
export type DetectMap<O extends KeyringOptions | undefined> = DetectPairType<O> extends 'ethereum'
? TestKeyringMapEthereum
: TestKeyringMapSubstrate;
export type DetectPairType<O extends KeyringOptions | undefined> = O extends KeyringOptions
? O['type'] extends KeypairType
? O['type']
: 'sr25519'
: 'sr25519';
export function createTestPairs <O extends KeyringOptions, M = DetectMap<O>> (options?: O, isDerived = true): M {
export function createTestPairs <O extends KeyringOptions, M = TestKeyringMapQuantus> (options?: O, isDerived = true): M {
const keyring = createTestKeyring(options, isDerived);
const pairs = keyring.getPairs();
const map: TestKeyringMap = { nobody: nobody() };

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HexString } from '@polkadot/util/types';
@@ -7,7 +7,7 @@ import type { EncryptedJson, Keypair, KeypairType, Prefix } from '@polkadot/util
export interface KeyringOptions {
/** The ss58Format to use for address encoding (defaults to 42) */
ss58Format?: Prefix;
/** The type of keyring to create (defaults to ed25519) */
/** The type of keyring to create (defaults to dilithium65, ML-DSA-65) */
type?: KeypairType;
}
@@ -72,6 +72,17 @@ export interface KeyringPair$Json extends EncryptedJson {
}
export interface SignOptions {
/**
* The FIPS 204 domain-separation context, for post-quantum pairs.
*
* Mandatory for `dilithium65`/`dilithium87` and ignored by every other type.
* There is deliberately no default: Quantus extrinsics on spec >= 148 are
* verified under `QUANTUS_EXTRINSIC` and earlier ones under the empty context,
* and a signature made under the wrong one is cryptographically valid, rejected
* by the chain, and locally indistinguishable from a correct one. Only the
* caller knows the spec version, so only the caller can answer.
*/
context?: Uint8Array;
/** Create a MultiSignature-compatible output with an indicator type */
withType?: boolean;
}
@@ -92,7 +103,11 @@ export interface KeyringPair {
sign (message: string | Uint8Array, options?: SignOptions): Uint8Array;
toJson (passphrase?: string): KeyringPair$Json;
unlock (passphrase?: string): void;
verify (message: string | Uint8Array, signature: Uint8Array, signerPublic: string | Uint8Array): boolean;
/**
* Verify an ML-DSA signature, bare or as the chain's `signature ‖ publicKey`,
* under `context` (default: the empty context used for raw bytes).
*/
verify (message: string | Uint8Array, signature: Uint8Array, signerPublic: string | Uint8Array, context?: Uint8Array): boolean;
vrfSign (message: string | Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): Uint8Array;
vrfVerify (message: string | Uint8Array, vrfResult: Uint8Array, signerPublic: string | Uint8Array, context?: string | Uint8Array, extra?: string | Uint8Array): boolean;
}

View File

@@ -15,14 +15,14 @@
},
"sideEffects": false,
"type": "module",
"version": "13.5.6",
"version": "14.0.3-quantus.3",
"main": "index.js",
"dependencies": {
"@polkadot/util": "13.5.6",
"@polkadot/util": "14.0.3",
"@substrate/ss58-registry": "^1.51.0",
"tslib": "^2.8.0"
},
"devDependencies": {
"@polkadot/hw-ledger": "13.5.6"
"@polkadot/hw-ledger": "14.0.3"
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/networks authors & contributors
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.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([]);
});

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/networks authors & contributors
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KnownGenesis } from '../types.js';
@@ -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

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/networks authors & contributors
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KnownIcon } from '../types.js';

View File

@@ -1,7 +1,8 @@
// Copyright 2017-2025 @polkadot/networks authors & contributors
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
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

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/networks authors & contributors
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KnownLedger } from '../types.js';

View File

@@ -0,0 +1,39 @@
// 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',
// HEI, not QTC — confirmed from the chain itself via system_properties on
// a1-heisenberg.quantus.cat. A testnet token that displayed as the mainnet
// symbol would be a good way to confuse play money for real.
symbols: ['HEI'],
website: 'https://quantus.com'
}
];

View File

@@ -1,9 +1,10 @@
// Copyright 2017-2025 @polkadot/networks authors & contributors
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
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

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/networks authors & contributors
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
@@ -6,7 +6,7 @@
import type { SubstrateNetwork } from './types.js';
import { knownGenesis, knownIcon, knownLedger, knownTestnet } from './defaults/index.js';
import { allNetworks, availableNetworks, selectableNetworks } from './index.js';
import { allNetworks, availableNetworks, isQuantumSafe, selectableNetworks } from './index.js';
describe('availableNetworks', (): void => {
it('has the correct starting order', (): void => {
@@ -51,16 +51,40 @@ 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 deliberately — see exposedTestnet — so what is
// worth pinning is that an exposed testnet is still *labelled* one. Losing the
// label is the failure that matters: 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
)
availableNetworks.some((a) => 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([]);
});
// These lists stay complete on purpose. An earlier version of this fork
// filtered them to post-quantum chains, which broke @polkadot/types-known — it
// looks up every chain it holds upgrade history for and throws when one is
// missing, so @polkadot/api failed at import. Gating what a wallet offers is
// the wallet's job; isQuantumSafe is exported for it and the extension applies
// it in its own chain list.
it('identifies which chains are post-quantum', (): void => {
expect(isQuantumSafe('ml-dsa')).toEqual(true);
expect(isQuantumSafe('Sr25519')).toEqual(false);
expect(isQuantumSafe(null)).toEqual(false);
expect(availableNetworks.filter((n) => isQuantumSafe(n.standardAccount)).map((n) => n.network)).toEqual(['quantus', 'quantus-heisenberg']);
});
it('has allNetworks icons, except for overrides', (): void => {
expect(
availableNetworks.filter(({ icon, network }) =>
@@ -92,14 +116,20 @@ describe('availableNetworks', (): void => {
});
describe('allNetworks', (): void => {
it('has no ss58 duplicates', (): void => {
// Upstream asserted one network per SS58 prefix. That stops holding the moment
// a chain has a testnet: Quantus mainnet and Heisenberg are both 189, because
// they are the same chain at two deployments and an address is valid on
// either. Consumers key on genesisHash, so sharing a prefix costs nothing.
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

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/networks authors & contributors
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
// TODO: This was removed, really cannot recall the reason...
@@ -7,3 +7,4 @@
export * from './interfaces.js';
export { packageInfo } from './packageInfo.js';
export { isQuantumSafe } from './quantumSafe.js';

View File

@@ -1,11 +1,11 @@
// Copyright 2017-2025 @polkadot/networks authors & contributors
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
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 &&
@@ -59,11 +68,31 @@ function sortNetworks (a: Network, b: Network): number {
: 1;
}
// This is all the Substrate networks with our additional information
export const allNetworks = knownSubstrate.map(toExpanded);
// This is all the Substrate networks with our additional information.
//
// None of these three lists is filtered to post-quantum chains, and an earlier
// version of this fork filtering `availableNetworks` was a mistake worth
// recording. These are *library* lists, not a product's offer: other packages
// consume them expecting the chains Substrate knows about to be present.
// `@polkadot/types-known` looks up every chain it has upgrade history for and
// **throws** when one is missing, so removing Polkadot and Kusama here made
// `@polkadot/api` fail at import — nothing to do with Quantus, and impossible to
// diagnose from the error.
//
// Gating what a wallet *offers* is the wallet's decision. `isQuantumSafe` is
// exported for exactly that, and the extension applies it in its own chain list.
// Keeping these complete also means a pasted Polkadot address still decodes and
// renders as what it is — recognisable, and therefore refusable.
//
// 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
// - testnets only where explicitly exposed (see exposedTestnet)
// - we only include those where we have a standardAccount
// - sort by name, however we keep 0, 2, 42 first in the list
export const availableNetworks = allNetworks.filter(filterAvailable).sort(sortNetworks);

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/networks authors & contributors
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
export * from './index.js';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/networks authors & contributors
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @polkadot/dev

View File

@@ -1,6 +1,6 @@
// Copyright 2017-2025 @polkadot/networks authors & contributors
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
// Do not edit, auto-generated by @polkadot/dev
export const packageInfo = { name: '@polkadot/networks', path: 'auto', type: 'auto', version: '13.5.6' };
export const packageInfo = { name: '@polkadot/networks', path: 'auto', type: 'auto', version: '14.0.3-quantus.3' };

View File

@@ -0,0 +1,32 @@
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KnownSubstrate } from './types.js';
/**
* The `standardAccount` values that survive a cryptographically relevant quantum
* computer.
*
* Everything else in the registry — `*25519`, `Ed25519`, `Sr25519`, `secp256k1` —
* is a discrete-log scheme, and Shor's algorithm breaks all of them at once. A
* chain using one of those has no quantum-safe account type to offer, whatever
* else is true of it.
*/
const QUANTUM_SAFE_ACCOUNTS: KnownSubstrate['standardAccount'][] = ['ml-dsa'];
/**
* Whether a network's accounts are post-quantum.
*
* This gates what the tooling built on this fork will *offer*. It deliberately
* does not gate address encoding or decoding: pasting a Polkadot address into a
* Quantus wallet should render as a Polkadot address so it can be recognised and
* refused, not fail to decode and look like a typo.
*
* A `null` standardAccount — which the registry uses for chains that never said —
* is treated as unsafe. Unknown is not the same as safe, and the cost of being
* wrong in that direction is a user holding funds under a key Shor's algorithm
* recovers.
*/
export function isQuantumSafe (standardAccount: KnownSubstrate['standardAccount']): boolean {
return QUANTUM_SAFE_ACCOUNTS.includes(standardAccount);
}

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/networks authors & contributors
// Copyright 2017-2026 @polkadot/networks authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/keyring authors & contributors
// Copyright 2017-2026 @polkadot/keyring authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { RegistryEntry } from '@substrate/ss58-registry';
@@ -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

@@ -20,7 +20,7 @@
"./packageDetect.cjs"
],
"type": "module",
"version": "13.5.6",
"version": "14.0.3-quantus.3",
"browser": {
"crypto": false,
"stream": false
@@ -29,16 +29,18 @@
"dependencies": {
"@noble/curves": "^1.3.0",
"@noble/hashes": "^1.3.3",
"@polkadot/networks": "13.5.6",
"@polkadot/util": "13.5.6",
"@polkadot/wasm-crypto": "^7.5.1",
"@polkadot/wasm-util": "^7.5.1",
"@polkadot/x-bigint": "13.5.6",
"@polkadot/x-randomvalues": "13.5.6",
"@polkadot/networks": "14.0.3-quantus.3",
"@polkadot/util": "14.0.3",
"@polkadot/wasm-crypto": "^7.5.3",
"@polkadot/wasm-util": "^7.5.3",
"@polkadot/x-bigint": "14.0.3",
"@polkadot/x-randomvalues": "14.0.3",
"@quantus/crypto": "^0.3.0",
"@scure/base": "^1.1.7",
"@scure/sr25519": "^0.2.0",
"tslib": "^2.8.0"
},
"peerDependencies": {
"@polkadot/util": "13.5.6"
"@polkadot/util": "14.0.3"
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { decodeAddress } from './decode.js';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Prefix } from './types.js';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { sshash } from './sshash.js';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Prefix } from './types.js';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { availableNetworks } from '../networks.js';

View File

@@ -1,26 +0,0 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />
import { waitReady } from '@polkadot/wasm-crypto';
import { deriveAddress } from './index.js';
describe('deriveAddress', (): void => {
beforeEach(async (): Promise<void> => {
await waitReady();
});
it('derives a known path', (): void => {
expect(
deriveAddress('5CZtJLXtVzrBJq1fMWfywDa6XuRwXekGdShPR4b8i9GWSbzB', '/joe/polkadot/0')
).toEqual('5GZ4srnepXvdsuNVoxCGyVZd8ScDm4gkGLTKuaGARy9akjTa');
});
it('fails on hard paths', (): void => {
expect(
() => deriveAddress('5CZtJLXtVzrBJq1fMWfywDa6XuRwXekGdShPR4b8i9GWSbzB', '//bob')
).toThrow(/Expected suri to contain a combination of non-hard paths/);
});
});

View File

@@ -1,36 +0,0 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { DeriveJunction } from '../key/DeriveJunction.js';
import type { Prefix } from './types.js';
import { keyExtractPath } from '../key/index.js';
import { sr25519DerivePublic } from '../sr25519/index.js';
import { decodeAddress } from './decode.js';
import { encodeAddress } from './encode.js';
function filterHard ({ isHard }: DeriveJunction): boolean {
return isHard;
}
/**
* @name deriveAddress
* @summary Creates a sr25519 derived address from the supplied and path.
* @description
* Creates a sr25519 derived address based on the input address/publicKey and the uri supplied.
*/
export function deriveAddress (who: string | Uint8Array, suri: string, ss58Format?: Prefix): string {
const { path } = keyExtractPath(suri);
if (!path.length || path.every(filterHard)) {
throw new Error('Expected suri to contain a combination of non-hard paths');
}
let publicKey = decodeAddress(who);
for (const { chainCode } of path) {
publicKey = sr25519DerivePublic(publicKey, chainCode);
}
return encodeAddress(publicKey, ss58Format);
}

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Prefix } from './types.js';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@polkadot/util';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { BN } from '@polkadot/util';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { u8aEq } from '@polkadot/util';

View File

@@ -1,4 +1,4 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
/// <reference types="@polkadot/dev-test/globals.d.ts" />

View File

@@ -1,13 +1,13 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HashType } from '../secp256k1/types.js';
import type { HashType } from './hasher.js';
import type { Prefix } from './types.js';
import { u8aConcat } from '@polkadot/util';
import { hasher } from '../secp256k1/hasher.js';
import { encodeAddress } from './encode.js';
import { hasher } from './hasher.js';
/**
* @name evmToAddress

View File

@@ -1,11 +1,12 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HashType } from './types.js';
/** Which hash an EVM ↔ Substrate address conversion uses. */
import { blake2AsU8a } from '../blake2/index.js';
import { keccakAsU8a } from '../keccak/index.js';
export type HashType = 'blake2' | 'keccak';
export function hasher (hashType: HashType, data: Uint8Array | string, onlyJs?: boolean): Uint8Array {
return hashType === 'keccak'
? keccakAsU8a(data, undefined, onlyJs)

View File

@@ -1,11 +1,10 @@
// Copyright 2017-2025 @polkadot/util-crypto authors & contributors
// Copyright 2017-2026 @polkadot/util-crypto authors & contributors
// SPDX-License-Identifier: Apache-2.0
export { addressToEvm } from './addressToEvm.js';
export { checkAddress } from './check.js';
export { checkAddressChecksum } from './checksum.js';
export { decodeAddress } from './decode.js';
export { deriveAddress } from './derive.js';
export { encodeAddress } from './encode.js';
export { encodeDerivedAddress } from './encodeDerived.js';
export { encodeMultiAddress } from './encodeMulti.js';

Some files were not shown because too many files have changed in this diff Show More