Files
rob thijssen 09a96b64bf feat: render account ids as SS58 when a prefix is set
A decoded call named its recipient as 32 bytes of hex. That is a correct
description of the value and the one form nobody reads — and the only moment in
a wallet where reading the recipient matters is the screen asking somebody to
approve sending them money.

`set_ss58_format` turns it on. Off by default, and deliberately: the prefix is a
property of the chain a caller is talking to rather than of the metadata, so
inferring one would put a plausible, wrong address in front of that same person.

Account types are found by their **registry path**, not by length. A block hash
is also 32 bytes, and rendering one as an address would be a lie a reader cannot
catch — there is a test that `System::BlockHash` stays hex with a prefix set.
`scale_value` carries each value's type id as its context, so the check is on
what the runtime declared.

The vector is crystal_bob on Heisenberg, taken from the chain rather than
computed here, which also pins the two-byte prefix form — 189 needs it, and
getting it wrong yields an address that looks right and belongs to nobody.

Refs #3, quantus/extension#6

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

@quantus/codec

Metadata-driven SCALE encode and decode for the Quantus chain, compiled to WASM.

Nothing in this package names a pallet, a call, a signed extension or a signature scheme. Everything is read from the metadata the node produced by running Metadata_metadata against the runtime WASM in a given block's state, which makes the runtime the oracle rather than this package's author.

Why not @polkadot/api

Three reasons, in increasing order of importance — the evidence is on quantus/api#1.

  1. @polkadot/types refuses fixed arrays longer than 2048 bytes. ML-DSA signatures are [u8;5261] and [u8;7219], so every Quantus extrinsic trips it.
  2. api.rpc.chain.getBlock throws on every block of this chain, at the timestamp inherent. The extrinsic preamble byte's top two bits are a type tag (0b00 bare, 0b10 signed, 0b01 general) and the low six are the version; Quantus emits 0x84 — signed, v4 — and 0x05 — bare, v5 — in the same block while the metadata declares version 4. polkadot-js reads that byte as a version.
  3. It guesses that signed extensions it does not recognise contribute nothing to the signed payload, logging Unknown signed extensions … treating them as no-effect.

The third is why this package exists rather than a patch. The guess is correct only while every unrecognised extension happens to be zero-sized. This chain's encoding has already changed between runtimes — transactionVersion has gone 2 → 3 → 6 across four upgrades, each an extrinsic-format change — and when the guess stops being correct the wallet keeps signing. Those signatures are cryptographically valid, over a payload missing bytes the runtime put there, and the chain reports them as BadProof, which is also what it reports for a wrong key. Silent, remote, and indistinguishable from the one thing it is not.

Here the registry decides. An extension whose declared type encodes to nothing contributes nothing; anything else must be supplied by the caller or no payload is produced at all.

Use

import { Runtime } from '@quantus/codec';

const runtime = Runtime.fromMetadata(await fetchMetadata()); // state_getMetadata

const call = runtime.encodeCall('Balances', 'transfer_keep_alive', {
  dest: { Id: '0x…' },
  value: '1000000000'
});

const values = runtime.standardExtensions({
  blockHash: genesisHash, // immortal era
  genesisHash,
  nonce,
  specVersion,
  transactionVersion
});

const payload = runtime.signerPayload(call, values);
// sign `payload` with @quantus/crypto under the QUANTUS_EXTRINSIC context,
// hashing it first with BLAKE2b-256 if it is longer than 256 bytes
const extrinsic = runtime.encodeExtrinsic(
  { Id: accountId },
  signature,
  runtime.encodeExtra(values),
  call
);

standardExtensions fills in the extensions Substrate itself defines. Anything else this runtime declares as non-empty is refused by name — see above for why that is the desired behaviour rather than a limitation.

Build

./scripts/build-quantus.sh quantus-codec

Same constraints as @quantus/crypto: a modern toolchain (separate from wasm-crypto's 2022 nightly), initSync over base64+zlib for the MV3 CSP, wasm-bindgen's own glue rather than @polkadot/wasm-bridge, and binaryen 123 — version 105 silently corrupts the output. See quantus/wasm#1 and #3.