feat: accept the Quantus signature type, and list Quantus chains

## The check that fired before code that would have worked

`getSignerType` pulls the extrinsic's `Address` and `Signature` types **out of
the metadata** — the runtime-as-oracle approach, and better than polkadot-js,
which hardcodes `ExtrinsicSignature: 'MultiSignature'` in a type definition file.
Then it threw the answer away:

    if (signature.type !== "enum" || ["Ecdsa", "Ed25519", "Sr25519"].some(...))
      throw unkownSignerType();

Quantus's `DilithiumSignatureScheme` carries `Dilithium87` and `Dilithium65`, so
this raised `Unkown signer` and nothing downstream ever ran. Asking the runtime
and then not believing it is the trap this whole port exists to avoid.

Everything past that point was already correct: `createV4Tx` concatenates and
compact-prefixes and never asserts a signature size, and the address prefix is
the `MultiAddress::Id` variant index read from metadata. The name whitelist only
separated the Substrate shape from the Ethereum one, and the Ethereum arm one
line above already decides that structurally. So the patch drops the names and
keeps `signature.type !== "enum"`.

pnpm `patchedDependencies`, keyed on the package name rather than a version, so
it reaches all fifteen resolved copies in the tree — the nested ones under
pjs-signer, ledger-signer, raw-tx-creator and polkadot-api itself are what
actually run.

## Evidence

Byte-for-byte against the quantus/extension#7 tier-1 harness, same call, same
nonce, same signature:

    papi assembled 7300 bytes
    IDENTICAL to the tier-1 harness

That is the corroboration this repo exists for. polkadot-api shares no code with
the five polkadot-js forks, so until now our stack had only agreed with itself.

Also verified the patch is load-bearing and narrow:

  - the unpatched copy still in the store raises `Unkown signer` on the same
    inputs, so the check really was the blocker
  - Polkadot metadata still takes the Substrate path, and still gets its `01`
    variant byte prepended when a caller names a signing type

## Networks

Quantus, Heisenberg and Planck, with the endpoints verified reachable on
2026-09-15, and Quantus as the default. The Substrate networks stay: a signature
that works on both is the only evidence that the patch widened a check rather
than broke one.

Refs #1, #3. Closes quantus/extension#7's cross-tier comparison.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
This commit is contained in:
2026-09-15 22:16:40 +03:00
parent ed2fd05ebe
commit 933ffecc8f
5 changed files with 103 additions and 19 deletions

View File

@@ -0,0 +1,30 @@
diff --git a/dist/v4.js b/dist/v4.js
index e8ec50ec7fb5d3dab4f30cbc21fe36d0416f8a9d..cddc959ac2f0f396f17c18680d3d3fb0ca310f46 100644
--- a/dist/v4.js
+++ b/dist/v4.js
@@ -21,8 +21,23 @@ const getSignerType = (metadata) => {
}
if (address.type === "AccountId20" && signature.type === "array" && signature.len === 65 && signature.value.type === "primitive" && signature.value.value === "u8")
return [1 /* Ethereum */, []];
- if (signature.type !== "enum" || ["Ecdsa", "Ed25519", "Sr25519"].some((x) => !(x in signature.value)))
- throw unkownSignerType();
+ // Quantus patch: the variant *names* are not load-bearing.
+ //
+ // Upstream required the signature enum to carry Ecdsa, Ed25519 and Sr25519.
+ // That whitelist exists only to tell the Substrate shape from the Ethereum
+ // one, and the Ethereum arm one line above already decides that structurally —
+ // AccountId20 plus a 65-byte array. Everything below this point is
+ // length-agnostic: createV4Tx concatenates whatever the signer returned and
+ // compact-prefixes the result.
+ //
+ // So this check fired before code that would have worked. Quantus's
+ // DilithiumSignatureScheme carries Dilithium87 and Dilithium65, and the
+ // metadata said so — getSignerType read the type out of the metadata and then
+ // threw the answer away in favour of three hardcoded names. Asking the runtime
+ // and then not believing it is the trap this whole port exists to avoid.
+ //
+ // See quantus/papi-console#1.
+ if (signature.type !== "enum") throw unkownSignerType();
if (address.type === "enum") {
const id = address.value["Id"];
if (id.type === "lookupEntry" && id.value.type === "AccountId32")

33
pnpm-lock.yaml generated
View File

@@ -5,6 +5,7 @@ settings:
excludeLinksFromLockfile: false
patchedDependencies:
'@polkadot-api/signers-common': 365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37
react18-json-view: 962696fac5fe7ad3e94fd36dad07521c6c3eafd52f219ab335d52438387f6b66
importers:
@@ -46,7 +47,7 @@ importers:
version: 0.4.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@polkadot-api/signers-common':
specifier: ^0.3.1
version: 0.3.1(rxjs@7.8.2)
version: 0.3.1(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/substrate-bindings':
specifier: ^0.21.1
version: 0.21.1
@@ -5022,7 +5023,7 @@ snapshots:
'@noble/hashes': 2.4.0
'@paraspell/descriptors': 14.3.4(polkadot-api@3.1.0(esbuild@0.28.1)(rxjs@7.8.2))
'@paraspell/sdk-core': 14.3.4(typescript@7.0.2)(zod@3.25.76)
'@polkadot-api/signers-common': 0.3.1(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.1(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/substrate-bindings': 0.21.1
'@polkadot-labs/hdkd': 0.0.31
'@polkadot-labs/hdkd-helpers': 0.0.33
@@ -5190,7 +5191,7 @@ snapshots:
dependencies:
'@ledgerhq/hw-transport': 6.35.5
'@polkadot-api/merkleize-metadata': 1.3.0
'@polkadot-api/signers-common': 0.3.0(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.0(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/substrate-bindings': 0.21.0
'@polkadot-api/tx-creator': 0.2.0(rxjs@7.8.2)
'@polkadot-api/utils': 0.4.0
@@ -5216,7 +5217,7 @@ snapshots:
'@polkadot-api/meta-signers@0.3.0':
dependencies:
'@polkadot-api/metadata-builders': 0.15.0
'@polkadot-api/signers-common': 0.3.0(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.0(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/substrate-bindings': 0.21.0
'@polkadot-api/tx-creator': 0.2.0(rxjs@7.8.2)
'@polkadot-api/utils': 0.4.0
@@ -5276,7 +5277,7 @@ snapshots:
'@polkadot-api/pjs-signer@0.8.0(rxjs@7.8.2)':
dependencies:
'@polkadot-api/metadata-builders': 0.15.0
'@polkadot-api/signers-common': 0.3.0(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.0(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/substrate-bindings': 0.21.0
'@polkadot-api/tx-creator': 0.2.0(rxjs@7.8.2)
'@polkadot-api/utils': 0.4.0
@@ -5286,7 +5287,7 @@ snapshots:
'@polkadot-api/pjs-signer@0.8.1(rxjs@7.8.2)':
dependencies:
'@polkadot-api/metadata-builders': 0.15.1
'@polkadot-api/signers-common': 0.3.1(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.1(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/substrate-bindings': 0.21.1
'@polkadot-api/tx-creator': 0.2.0(rxjs@7.8.2)
'@polkadot-api/utils': 0.4.0
@@ -5302,7 +5303,7 @@ snapshots:
'@noble/hashes': 2.4.0
'@polkadot-api/merkleize-metadata': 1.3.0
'@polkadot-api/metadata-builders': 0.15.0
'@polkadot-api/signers-common': 0.3.0(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.0(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/substrate-bindings': 0.21.0
'@polkadot-api/tx-creator': 0.2.0(rxjs@7.8.2)
'@polkadot-api/utils': 0.4.0
@@ -5313,7 +5314,7 @@ snapshots:
'@noble/hashes': 2.4.0
'@polkadot-api/merkleize-metadata': 1.3.1
'@polkadot-api/metadata-builders': 0.15.1
'@polkadot-api/signers-common': 0.3.1(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.1(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/substrate-bindings': 0.21.1
'@polkadot-api/tx-creator': 0.2.0(rxjs@7.8.2)
'@polkadot-api/utils': 0.4.0
@@ -5335,7 +5336,7 @@ snapshots:
react: 19.2.8
react-dom: 19.2.8(react@19.2.8)
'@polkadot-api/signers-common@0.3.0(rxjs@7.8.2)':
'@polkadot-api/signers-common@0.3.0(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)':
dependencies:
'@polkadot-api/metadata-builders': 0.15.0
'@polkadot-api/substrate-bindings': 0.21.0
@@ -5343,7 +5344,7 @@ snapshots:
'@polkadot-api/utils': 0.4.0
rxjs: 7.8.2
'@polkadot-api/signers-common@0.3.1(rxjs@7.8.2)':
'@polkadot-api/signers-common@0.3.1(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)':
dependencies:
'@polkadot-api/metadata-builders': 0.15.1
'@polkadot-api/substrate-bindings': 0.21.1
@@ -5414,7 +5415,7 @@ snapshots:
dependencies:
'@polkadot-api/merkleize-metadata': 1.3.0
'@polkadot-api/metadata-builders': 0.15.0
'@polkadot-api/signers-common': 0.3.0(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.0(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/substrate-bindings': 0.21.0
'@polkadot-api/utils': 0.4.0
transitivePeerDependencies:
@@ -5424,7 +5425,7 @@ snapshots:
dependencies:
'@polkadot-api/merkleize-metadata': 1.3.1
'@polkadot-api/metadata-builders': 0.15.1
'@polkadot-api/signers-common': 0.3.1(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.1(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/substrate-bindings': 0.21.1
'@polkadot-api/utils': 0.4.0
transitivePeerDependencies:
@@ -5761,7 +5762,7 @@ snapshots:
'@polkahub/plugin@0.9.1(@react-rxjs/core@0.10.8(react@19.2.8)(rxjs@7.8.2))(esbuild@0.28.1)(react@19.2.8)':
dependencies:
'@polkadot-api/signers-common': 0.3.0(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.0(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/tx-creator': 0.2.0(rxjs@7.8.2)
'@react-rxjs/utils': 0.9.7(@react-rxjs/core@0.10.8(react@19.2.8)(rxjs@7.8.2))(react@19.2.8)(rxjs@7.8.2)
'@rx-state/core': 0.1.4(rxjs@7.8.2)
@@ -5892,7 +5893,7 @@ snapshots:
'@polkahub/vault@0.9.1(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
dependencies:
'@polkadot-api/merkleize-metadata': 1.3.1
'@polkadot-api/signers-common': 0.3.0(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.0(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/substrate-bindings': 0.21.0
'@polkahub/context': 0.9.1(@types/react@19.2.18)(esbuild@0.28.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
'@polkahub/plugin': 0.9.1(@react-rxjs/core@0.10.8(react@19.2.8)(rxjs@7.8.2))(esbuild@0.28.1)(react@19.2.8)
@@ -9234,7 +9235,7 @@ snapshots:
'@polkadot-api/observable-client': 0.19.0(rxjs@7.8.2)
'@polkadot-api/pjs-signer': 0.8.0(rxjs@7.8.2)
'@polkadot-api/raw-tx-creator': 0.4.0(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.0(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.0(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/sm-provider': 0.3.8(@polkadot-api/smoldot@0.4.7)
'@polkadot-api/smoldot': 0.4.7
'@polkadot-api/substrate-bindings': 0.21.0
@@ -9263,7 +9264,7 @@ snapshots:
'@polkadot-api/observable-client': 0.19.1(rxjs@7.8.2)
'@polkadot-api/pjs-signer': 0.8.1(rxjs@7.8.2)
'@polkadot-api/raw-tx-creator': 0.4.1(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.1(rxjs@7.8.2)
'@polkadot-api/signers-common': 0.3.1(patch_hash=365a34617de2ae3ad2f7e009f7f3e20e0078448fbfc93c395d323f0e21b75d37)(rxjs@7.8.2)
'@polkadot-api/sm-provider': 0.3.8(@polkadot-api/smoldot@0.4.7)
'@polkadot-api/smoldot': 0.4.7
'@polkadot-api/substrate-bindings': 0.21.1

View File

@@ -9,4 +9,5 @@ minimumReleaseAgeExclude:
- '@paraspell/sdk-core@14.3.0 || 14.3.4'
- '@paraspell/sdk@14.3.0 || 14.3.4'
patchedDependencies:
'@polkadot-api/signers-common': patches/@polkadot-api__signers-common.patch
react18-json-view: patches/react18-json-view.patch

View File

@@ -1,6 +1,7 @@
import ksmRawNetworks from "./kusama.json"
import paseoRawNetworks from "./paseo.json"
import polkadotRawNetworks from "./polkadot.json"
import quantusRawNetworks from "./quantus.json"
import westendRawNetworks from "./westend.json"
export type Network = {
@@ -16,12 +17,13 @@ export type NetworkCategory = {
networks: Network[]
}
const [Polkadot, Kusama, Paseo, Westend] = (
const [Polkadot, Kusama, Paseo, Westend, Quantus] = (
[
polkadotRawNetworks,
ksmRawNetworks,
paseoRawNetworks,
westendRawNetworks,
quantusRawNetworks,
] as const
).map((n): Network[] =>
n.map((x) => ({
@@ -29,11 +31,20 @@ const [Polkadot, Kusama, Paseo, Westend] = (
lightclient: !!x.hasChainSpecs,
id: x.id,
display: x.display,
relayChain: x.relayChainInfo?.id,
// Quantus is a solo chain and its entries carry no `relayChainInfo`, so the
// union of the imported JSON literals no longer has the property on every
// arm. Narrowed rather than cast: a typo in one of the relay-chain files
// should still be a type error.
relayChain: "relayChainInfo" in x ? x.relayChainInfo?.id : undefined,
})),
)
// Quantus first, and the default below. This console exists to exercise a
// post-quantum chain; the Substrate networks stay because a signature that works
// on both is the only evidence that the patch in patches/ widened a check rather
// than broke one.
const networks = {
Quantus,
Polkadot,
Kusama,
Paseo,
@@ -69,4 +80,5 @@ export const addCustomNetwork = (uri: string) => {
getCustomNetwork().endpoints[uri] = uri
}
export const defaultNetwork = Polkadot[0]
// Mainnet. The console opens on the chain it was forked for.
export const defaultNetwork = Quantus[0]

View File

@@ -0,0 +1,40 @@
[
{
"id": "quantus",
"display": "Quantus",
"rpcs": {
"Quantus": "wss://rpc1-mainnet.quantus.com"
},
"nativeToken": {
"symbol": "QTC",
"decimals": 12
},
"hasChainSpecs": false
},
{
"id": "heisenberg",
"display": "Heisenberg",
"rpcs": {
"a1": "wss://a1-heisenberg.quantus.cat",
"a2": "wss://a2-heisenberg.quantus.cat"
},
"nativeToken": {
"symbol": "HEI",
"decimals": 12
},
"hasChainSpecs": false
},
{
"id": "planck",
"display": "Planck",
"rpcs": {
"a1": "wss://a1-planck.quantus.cat",
"a2": "wss://a2-planck.quantus.cat"
},
"nativeToken": {
"symbol": "PLK",
"decimals": 12
},
"hasChainSpecs": false
}
]