Build against the Quantus forks instead of the published @polkadot packages #2

Closed
opened 2026-09-10 10:14:14 +00:00 by grenade · 3 comments
Owner

Part of #1. Nothing else in this repo can be tested until this lands.

What

The root package.json already pins the whole @polkadot surface through resolutions:

"resolutions": {
  "@polkadot/api": "^16.5.6",
  "@polkadot/keyring": "^14.0.3",
  "@polkadot/networks": "^14.0.3",
  "@polkadot/types": "^16.5.6",
  "@polkadot/util": "^14.0.3",
  "@polkadot/util-crypto": "^14.0.3",
  
}

Point the ones we fork at quantus/common, quantus/ui, quantus/wasm and — only if quantus/api#1 concludes it is needed — quantus/api. Everything else keeps coming from npm.

Decide how the forks are consumed

Three options, and this issue should settle it rather than leave each developer to guess:

  1. Git dependencies — simplest, no publishing step, but yarn resolution of a monorepo subpackage from a git URL is awkward and the forks are polkadot-dev workspaces that need building.
  2. A private npm registry — Gitea has a package registry; publish @quantus/keyring etc. and depend on versions. Most conventional, needs auth in CI.
  3. Yarn portal:/workspace links for local dev, published artifacts for CI — probably where this ends up, but say so explicitly.

Whatever is chosen, yarn build:chrome and yarn build:ff must work from a clean checkout, and the extension's own release build must not require a developer to have the fork repos checked out beside it.

Also

  • Both manifests already set script-src 'self' 'wasm-unsafe-eval', so the wasm from quantus/wasm#1 needs no manifest change — confirm this rather than assuming it, since the background context is an MV3 service worker, not a page.
  • Extension branding: name, description, icons, and the polkadot{.js} strings in manifest_chrome.json / manifest_firefox.json. Cosmetic, but it decides whether a user can tell this apart from the upstream extension when both are installed, which they will be.
  • Keep a documented git remote for upstream polkadot-js/extension. The rebasability convention in #1 applies here too.
Part of #1. Nothing else in this repo can be tested until this lands. ## What The root `package.json` already pins the whole `@polkadot` surface through `resolutions`: ```json "resolutions": { "@polkadot/api": "^16.5.6", "@polkadot/keyring": "^14.0.3", "@polkadot/networks": "^14.0.3", "@polkadot/types": "^16.5.6", "@polkadot/util": "^14.0.3", "@polkadot/util-crypto": "^14.0.3", … } ``` Point the ones we fork at [quantus/common](../../../common), [quantus/ui](../../../ui), [quantus/wasm](../../../wasm) and — only if quantus/api#1 concludes it is needed — [quantus/api](../../../api). Everything else keeps coming from npm. ## Decide how the forks are consumed Three options, and this issue should settle it rather than leave each developer to guess: 1. **Git dependencies** — simplest, no publishing step, but yarn resolution of a monorepo subpackage from a git URL is awkward and the forks are `polkadot-dev` workspaces that need building. 2. **A private npm registry** — Gitea has a package registry; publish `@quantus/keyring` etc. and depend on versions. Most conventional, needs auth in CI. 3. **Yarn `portal:`/workspace links for local dev, published artifacts for CI** — probably where this ends up, but say so explicitly. Whatever is chosen, `yarn build:chrome` and `yarn build:ff` must work from a clean checkout, and the extension's own release build must not require a developer to have the fork repos checked out beside it. ## Also - Both manifests already set `script-src 'self' 'wasm-unsafe-eval'`, so the wasm from quantus/wasm#1 needs no manifest change — confirm this rather than assuming it, since the background context is an MV3 **service worker**, not a page. - Extension branding: name, description, icons, and the `polkadot{.js}` strings in `manifest_chrome.json` / `manifest_firefox.json`. Cosmetic, but it decides whether a user can tell this apart from the upstream extension when both are installed, which they will be. - Keep a documented `git remote` for upstream polkadot-js/extension. The rebasability convention in #1 applies here too.
Author
Owner

The "how are the forks consumed" question is answered and in place. Option 2 — a private registry.

Published

Gitea's npm registry at https://git.lair.cafe/api/packages/quantus/npm/:

package version
@quantus/crypto 0.1.0
@polkadot/keyring 14.0.3
@polkadot/util-crypto 14.0.3
@polkadot/networks 14.0.3

Reads are anonymous — verified — so a consumer needs no credential to install. Publishing uses pass gitea/package-publisher-token, which is now in the agent credential store with its blast radius documented.

@polkadot/util is deliberately not published: this fork does not modify it, so it comes from npm like the rest of the scope.

Two different mechanisms, for a reason

@quantus is routed as a whole scope in .yarnrc.yml:

npmScopes:
  quantus:
    npmRegistryServer: "https://git.lair.cafe/api/packages/quantus/npm/"

Safe, because we own every name in that scope.

@polkadot is not, and must not be. Most of that scope is unforked — util, dev, dev-ts, ui-settings, x-*, wasm-crypto and more — so routing it would send yarn looking for dozens of packages that are not there. The three forks are pinned individually to tarball URLs in resolutions:

"@polkadot/keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3/keyring-14.0.3.tgz"

Worth knowing when this repo is wired up: the extension will need the same split. It depends on far more of the @polkadot scope than ui does.

What came out

quantus/ui carried four workarounds, all for one cause — node resolves a portal or link symlink to its realpath, after which @polkadot/util-crypto resolved inside common's tree, where it is a workspace source directory with no exports map. All four are gone:

  • resolutions pointing at ../common/packages/*/build
  • tsconfig.base.json *.d.ts path mappings
  • tsconfig.json runtime .js overrides for @polkadot/dev-ts
  • --preserve-symlinks in the test scripts

tsconfig.base.json and tsconfig.json are now byte-identical to upstream again, which is the rebasability convention honoured rather than merely stated.

Verified

  • common: build clean, 3084 tests pass
  • ui: build clean, 46 tests pass including the 5 ML-DSA ones, and the installed @polkadot/util-crypto is confirmed to be the fork (has dilithium/, not a symlink)
  • ui lint: 0 errors, down from 25 on pristine upstream — those were no-unsafe-assignment/no-unsafe-argument on values that were any only because the types never resolved

One operational note

A version cannot be overwritten in place — npm refuses with "You cannot publish over the previously published versions". Republishing during development is delete-then-publish:

curl -X DELETE -H "Authorization: token $T" \
  "https://git.lair.cafe/api/v1/packages/quantus/npm/@polkadot%2Fkeyring/14.0.3"

The token can do this (write:package covers delete, returns 204). Worth deciding before long whether fork builds get a distinguishing version — 14.0.3-quantus.1 or similar — rather than shadowing upstream's number. Publishing under upstream's exact version is unambiguous in a lockfile because the URL is recorded, but it reads confusingly in yarn why.

The "how are the forks consumed" question is answered and in place. **Option 2 — a private registry.** ## Published Gitea's npm registry at `https://git.lair.cafe/api/packages/quantus/npm/`: | package | version | |---|---| | `@quantus/crypto` | 0.1.0 | | `@polkadot/keyring` | 14.0.3 | | `@polkadot/util-crypto` | 14.0.3 | | `@polkadot/networks` | 14.0.3 | **Reads are anonymous** — verified — so a consumer needs no credential to install. Publishing uses `pass gitea/package-publisher-token`, which is now in the agent credential store with its blast radius documented. `@polkadot/util` is deliberately **not** published: this fork does not modify it, so it comes from npm like the rest of the scope. ## Two different mechanisms, for a reason **`@quantus` is routed as a whole scope** in `.yarnrc.yml`: ```yaml npmScopes: quantus: npmRegistryServer: "https://git.lair.cafe/api/packages/quantus/npm/" ``` Safe, because we own every name in that scope. **`@polkadot` is not, and must not be.** Most of that scope is unforked — `util`, `dev`, `dev-ts`, `ui-settings`, `x-*`, `wasm-crypto` and more — so routing it would send yarn looking for dozens of packages that are not there. The three forks are pinned individually to tarball URLs in `resolutions`: ```json "@polkadot/keyring": "https://git.lair.cafe/api/packages/quantus/npm/%40polkadot%2Fkeyring/-/14.0.3/keyring-14.0.3.tgz" ``` Worth knowing when this repo is wired up: **the extension will need the same split.** It depends on far more of the `@polkadot` scope than `ui` does. ## What came out `quantus/ui` carried four workarounds, all for one cause — node resolves a portal or link symlink to its realpath, after which `@polkadot/util-crypto` resolved inside *common's* tree, where it is a workspace source directory with no `exports` map. All four are gone: - resolutions pointing at `../common/packages/*/build` - `tsconfig.base.json` `*.d.ts` path mappings - `tsconfig.json` runtime `.js` overrides for `@polkadot/dev-ts` - `--preserve-symlinks` in the test scripts `tsconfig.base.json` and `tsconfig.json` are now **byte-identical to upstream** again, which is the rebasability convention honoured rather than merely stated. ## Verified - `common`: build clean, **3084 tests pass** - `ui`: build clean, **46 tests pass** including the 5 ML-DSA ones, and the installed `@polkadot/util-crypto` is confirmed to be the fork (has `dilithium/`, not a symlink) - `ui` lint: **0 errors, down from 25 on pristine upstream** — those were `no-unsafe-assignment`/`no-unsafe-argument` on values that were `any` only because the types never resolved ## One operational note A version **cannot be overwritten in place** — npm refuses with *"You cannot publish over the previously published versions"*. Republishing during development is delete-then-publish: ```sh curl -X DELETE -H "Authorization: token $T" \ "https://git.lair.cafe/api/v1/packages/quantus/npm/@polkadot%2Fkeyring/14.0.3" ``` The token can do this (`write:package` covers delete, returns `204`). Worth deciding before long whether fork builds get a distinguishing version — `14.0.3-quantus.1` or similar — rather than shadowing upstream's number. Publishing under upstream's exact version is unambiguous in a lockfile because the URL is recorded, but it reads confusingly in `yarn why`.
Author
Owner

Done. Branch quantus-registry (bff8eec1). yarn build:chrome completes with post-quantum crypto in the bundle, and 65 tests pass.

Registry contents

package version
@quantus/crypto 0.1.1
@polkadot/keyring 14.0.3-quantus.1
@polkadot/util-crypto 14.0.3-quantus.1
@polkadot/networks 14.0.3-quantus.1
@polkadot/ui-keyring 3.16.7-quantus.1

Everything else — api, types, rpc-provider, util, dev, x-*, hw-ledger — still comes from npm, which is why @polkadot is pinned per-package rather than scope-routed.

The bundler bug none of our tests could have caught

The first build:chrome failed:

Module not found: Error: Can't resolve 'quantus_crypto_bg.wasm'
  in node_modules/@quantus/crypto/generated

wasm-bindgen's async __wbg_init contains new URL('quantus_crypto_bg.wasm', import.meta.url), and webpack resolves that statically at build time whether or not the branch can run. The file isn't in the package — the wasm ships base64'd in bytes.js, which is the package's whole purpose.

Ten Rust tests, twelve consumer assertions and a browser probe all passed while the package was unusable in a bundler. Fixed in quantus/wasm (e8bf9e20) by stripping the dead init after bindgen, with assertions so a wasm-bindgen upgrade fails the build rather than silently shipping it again.

The lesson worth keeping: this package's consumers bundle, and nothing in its own test suite does.

One upstream test rewritten

buildHierarchy.spec.ts failed on account ordering. compareByNetwork resolves a genesis hash through getNetworkMap()selectableNetworks, which this fork narrowed to post-quantum chains — so Polkadot and Kusama both resolved to '' and compared equal. The network-ordering tests were passing without ordering anything.

Rewritten with Quantus and Heisenberg, whose display names sort in the same direction the originals did, so the fixtures keep their shape. (I got the direction wrong on the first attempt and had to redo it — 'Quantus' < 'Quantus Heisenberg' matches 'Kusama' < 'Polkadot', not the reverse.)

Verified in the artifacts, not assumed

  • background.js (1.1 MB) — QUANTUS_EXTRINSIC, the dilithium keypair types, ext_poseidon_hash, ext_mldsa_sign, and the base64 wasm payload
  • extension.js (3.5 MB) — the Quantus network entry, QTC, the mainnet genesis hash

The split is correct: signing lives in the service worker, the UI only renders addresses.

Still open on this issue

The branding item from the original body — name, description, icons, and the polkadot{.js} strings in both manifests. Untouched. It decides whether a user can tell this apart from the upstream extension when both are installed, which they will be.

Also still true: no CI publishes these packages. Today it is a manual npm publish --tag quantus per package against pass gitea/package-publisher-token. Fine while one person is building; worth a Gitea Actions workflow before a second.

Done. Branch `quantus-registry` (`bff8eec1`). **`yarn build:chrome` completes with post-quantum crypto in the bundle**, and 65 tests pass. ## Registry contents | package | version | |---|---| | `@quantus/crypto` | 0.1.1 | | `@polkadot/keyring` | 14.0.3-quantus.1 | | `@polkadot/util-crypto` | 14.0.3-quantus.1 | | `@polkadot/networks` | 14.0.3-quantus.1 | | `@polkadot/ui-keyring` | 3.16.7-quantus.1 | Everything else — `api`, `types`, `rpc-provider`, `util`, `dev`, `x-*`, `hw-ledger` — still comes from npm, which is why `@polkadot` is pinned per-package rather than scope-routed. ## The bundler bug none of our tests could have caught The first `build:chrome` failed: ``` Module not found: Error: Can't resolve 'quantus_crypto_bg.wasm' in node_modules/@quantus/crypto/generated ``` wasm-bindgen's async `__wbg_init` contains `new URL('quantus_crypto_bg.wasm', import.meta.url)`, and **webpack resolves that statically at build time** whether or not the branch can run. The file isn't in the package — the wasm ships base64'd in `bytes.js`, which is the package's whole purpose. Ten Rust tests, twelve consumer assertions and a browser probe all passed while the package was unusable in a bundler. Fixed in quantus/wasm (`e8bf9e20`) by stripping the dead init after bindgen, with assertions so a wasm-bindgen upgrade fails the build rather than silently shipping it again. The lesson worth keeping: **this package's consumers bundle, and nothing in its own test suite does.** ## One upstream test rewritten `buildHierarchy.spec.ts` failed on account ordering. `compareByNetwork` resolves a genesis hash through `getNetworkMap()` → `selectableNetworks`, which this fork narrowed to post-quantum chains — so Polkadot and Kusama both resolved to `''` and compared equal. The network-ordering tests were passing **without ordering anything**. Rewritten with Quantus and Heisenberg, whose display names sort in the same direction the originals did, so the fixtures keep their shape. (I got the direction wrong on the first attempt and had to redo it — `'Quantus' < 'Quantus Heisenberg'` matches `'Kusama' < 'Polkadot'`, not the reverse.) ## Verified in the artifacts, not assumed - `background.js` (1.1 MB) — `QUANTUS_EXTRINSIC`, the dilithium keypair types, `ext_poseidon_hash`, `ext_mldsa_sign`, and the base64 wasm payload - `extension.js` (3.5 MB) — the Quantus network entry, `QTC`, the mainnet genesis hash The split is correct: signing lives in the service worker, the UI only renders addresses. ## Still open on this issue The **branding** item from the original body — name, description, icons, and the `polkadot{.js}` strings in both manifests. Untouched. It decides whether a user can tell this apart from the upstream extension when both are installed, which they will be. Also still true: no CI publishes these packages. Today it is a manual `npm publish --tag quantus` per package against `pass gitea/package-publisher-token`. Fine while one person is building; worth a Gitea Actions workflow before a second.
Author
Owner

Verified against main, closing.

  • Forks from the registry: .yarnrc.yml routes the @quantus scope, and package.json pins keyring, networks, ui-keyring and util-crypto to the -quantus.2 tarballs (bff8eec1). Nothing points at local fork checkouts.
  • WASM under the extension's CSP: both manifests keep 'wasm-unsafe-eval'. @quantus/crypto runs in the Firefox background page and in the Chrome MV3 service worker, since wallets were created and used in both browsers.
  • Branding: done in #15 (e0aa7836); 851500a5 fixed the manifest author and homepage_url it left behind.
  • Upstream: the README's first paragraph links polkadot-js/extension as the upstream. package.json authorship stays upstream's, as credit for the code.
Verified against `main`, closing. - **Forks from the registry:** `.yarnrc.yml` routes the `@quantus` scope, and `package.json` pins keyring, networks, ui-keyring and util-crypto to the `-quantus.2` tarballs (`bff8eec1`). Nothing points at local fork checkouts. - **WASM under the extension's CSP:** both manifests keep `'wasm-unsafe-eval'`. `@quantus/crypto` runs in the Firefox background page and in the Chrome MV3 service worker, since wallets were created and used in both browsers. - **Branding:** done in #15 (`e0aa7836`); `851500a5` fixed the manifest `author` and `homepage_url` it left behind. - **Upstream:** the README's first paragraph links polkadot-js/extension as the upstream. `package.json` authorship stays upstream's, as credit for the code.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: quantus/extension#2