Files
rob thijssen a29a68d48a feat: add the tier 2 harness dapp
quantus/extension#7 tier 2: a page that asks the *extension* to sign, then
assembles and submits the extrinsic itself. `yarn tier2` bundles it; the README
has the run steps and where to get a funded dev account.

Every intermediate value is printed — the call, the extra, the payload that was
signed, the signature that came back, the assembled extrinsic, and the round trip
decoded back out of it. That display is the debugger when a node rejects
something, and it is exactly what a real app hides behind a success toast.

Nothing in it uses @polkadot/api. @quantus/codec produces every byte and the
WebSocket is a JSON-RPC client that decodes nothing.

Also extracts decodeMethod out of Extrinsic.tsx into util/, with tests. It moved
from `registry.createType('Call', data)` to the runtime in the previous commit and
was the one changed path with no coverage — `yarn test` skips .spec.tsx, so as a
component method it would have stayed that way. The tests pin the three cases
that matter more than the happy path: metadata from a different spec version
decodes a call into something plausible and wrong and must be refused; a chain
with no runtime renders hex; and bytes that are not a call must not throw, since
an exception there lands between somebody and their funds.

Verified so far in a browser: the page loads under Firefox with the extension
installed, and @quantus/codec parses Heisenberg's 101,493-byte metadata client
side and reports extrinsic v4 with all twelve signed extensions. The injected
signing round trip is **not** yet verified end to end — driving the extension's
own UI needs a privileged browsing context this tooling cannot script — so the
popup half of tier 2 still wants a human at a keyboard. The background half is
covered by Extension.spec.ts, which runs the full pub(extrinsic.sign) ->
pri(signing.approve.password) path.

Refs #7

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
2026-09-15 14:57:53 +03:00
..
2026-09-15 14:57:53 +03:00
2026-09-15 14:57:53 +03:00
2026-09-15 14:57:53 +03:00

Tier 2 harness

quantus/extension#7 tier 2: a dapp that asks the extension to sign, then assembles and submits the extrinsic itself.

The point is not the UI. Every intermediate value is printed — the call, the extra, the payload that was signed, the signature that came back, the assembled extrinsic, and the round trip decoded from it. When a node rejects something, that display is the debugger, and it is exactly what a real app hides behind a success toast.

Nothing here uses @polkadot/api. @quantus/codec produces every byte, from metadata the node generated by running Metadata_metadata against the runtime WASM; the WebSocket is a JSON-RPC transport and decodes nothing. See quantus/api#1 for why.

Run it

yarn build                 # the extension, into packages/extension/build
yarn tier2                 # bundles scripts/tier2/src/harness.js
python3 -m http.server 8731

Then load packages/extension/build as an unpacked extension — about:debugging#/runtime/this-firefoxLoad Temporary Add-on, or chrome://extensions with developer mode on → Load unpacked — and open http://localhost:8731/scripts/tier2/index.html.

It has to be served over HTTP. The extension does not inject into file:// pages, so web3Enable returns nothing there.

A funded account

The dev genesis endows three accounts from trivial seeds (primitives/dilithium-crypto/src/pair.rs in the chain):

account seed scheme
crystal_alice 32 × 0x00 ML-DSA-87
dilithium_bob 32 × 0x01 ML-DSA-87
crystal_charlie 32 × 0x02 ML-DSA-87

Import one with Import account from pre-existing seed and the raw hex seed 0x0000…00. On Heisenberg crystal_alice is qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7.

Two things that makes it worth using early: these are ML-DSA-87, the legacy scheme, where new accounts are ML-DSA-65; and it is the non-HD path, a raw 32-byte seed straight into keygen with no derivation, which is a different code path from the mnemonic one.

What the buttons do

  • connect — fetches the runtime version, genesis hash and metadata, and lists the signed extensions the runtime declares. Two of them, ReversibleTransactionExtension and WormholeProofRecorderExtension, exist on no other chain.
  • list accountsweb3Enable then web3Accounts. The first call raises an authorization request in the extension.
  • signRaw — the bytes channel. Signs under the empty FIPS 204 context, so the result can never be replayed as an extrinsic.
  • build, sign and submit — provides this chain's metadata to the extension, builds the call, asks the extension to sign, assembles the extrinsic and submits it.

That metadata step is not optional. The extension refuses to sign an extrinsic for a chain it has no metadata for, because without the runtime's own description of its signed extensions it would be guessing at the payload — and a signature over the wrong bytes comes back from a node as BadProof, which is also what it says about a wrong key.

Tier 1

../tier1/submit.mjs does the same thing with no browser and no extension, signing with the forked keyring directly. It is the faster loop, and the one that answers "is this a crypto bug or a plumbing bug".

node scripts/tier1/submit.mjs
QUANTUS_WS=wss://… QUANTUS_DEST=qzk… node scripts/tier1/submit.mjs