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
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-firefox → Load 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,
ReversibleTransactionExtensionandWormholeProofRecorderExtension, exist on no other chain. - list accounts —
web3Enablethenweb3Accounts. 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