build(quantus-crypto): buildable, installable and tested as a package
The JS build now runs end to end and the built package has been consumed the way quantus/common will consume it. Four things had to be worked out. polkadot-dev-build-ts will not build this package. It returns early for any name not starting with @polkadot/, in both buildJs and when collecting locals for import rewriting. Renaming into someone else's scope to satisfy a string check would be worse than not using the tool, and nothing is lost: this package needs no deno variant, no rollup bundle, no cross-package import rewriting. A plain tsc build lives in scripts/build-quantus-js.sh, which also keeps `yarn build:js` byte-identical to upstream's behaviour. binaryen 105 silently breaks the wasm. Upstream pins version_105 (2021), which predates the externref tables wasm-bindgen 0.2.128 emits; wasm-opt "optimises" the table into something that fails at instantiation with `WebAssembly.Table. grow(): failed to grow table by 4`. The wasm is valid before wasm-opt and broken after, every cargo test still passes, and it only surfaces when a consumer tries to init. install-build-deps.sh now fetches binaryen 123 alongside, exactly as it does a second wasm-bindgen. The wasm-util dependency is imported deeply. Its package index re-exports packageDetect, whose only job is a side effect registering with @polkadot/util — a peer dependency we would inherit for nothing. base64 and fflate are pure functions with no dependencies, so the deep paths are both lighter and honest. ESM only, and the CJS scaffolding is removed. The consumers are ESM and the wasm-bindgen glue is ESM-only, so a CJS variant would mean a second generated glue or hand-written marshalling. Revisit if quantus/common's CJS build needs it. Also: the pack step must run after tsc, which clears build/; the checked-in bindings are refreshed by the build so they cannot drift; and both test suites are wired into the repo's test script, which previously ran wasm-crypto's only. The consumer test stages a real node_modules layout rather than testing in place, because in this repo node_modules/@polkadot/wasm-util symlinks to the package source, which carries no exports map — so a deep import resolves for a real consumer and fails here for reasons that have nothing to do with our package. Staging tests module resolution too, which is half of what can break in a published package. It is also what caught the binaryen fault. Post-wasm-opt: 234,292 raw / 109,649 zlib / 146,200 base64 — smaller than upstream's entire wasm-crypto blob (335,277 / 168,782 / 225,044). Refs quantus/wasm#1 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012uDUodEcRbBwNRi3UCmw8f
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,8 +1,10 @@
|
||||
binaryen/
|
||||
binaryen-quantus/
|
||||
bindgen/
|
||||
bindgen-quantus/
|
||||
build/
|
||||
build-*/
|
||||
build-test/
|
||||
bytes/
|
||||
coverage/
|
||||
node_modules/
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
"scripts": {
|
||||
"build": "yarn build:wasm",
|
||||
"build:js": "./scripts/build-js.sh",
|
||||
"build:quantus": "./scripts/build-quantus.sh",
|
||||
"build:quantus:js": "./scripts/build-quantus-js.sh",
|
||||
"build:release": "polkadot-ci-ghact-build",
|
||||
"build:rollup": "polkadot-exec-rollup --config",
|
||||
"build:wasm": "./scripts/build.sh",
|
||||
@@ -34,8 +36,10 @@
|
||||
"deno:check": "deno check --import-map=import_map.json mod.ts",
|
||||
"lint": "polkadot-dev-run-lint",
|
||||
"postinstall": "polkadot-dev-yarn-only",
|
||||
"test": "yarn test:wasm-crypto:rust",
|
||||
"test": "yarn test:wasm-crypto:rust && yarn test:quantus-crypto:rust",
|
||||
"test:js": "yarn test:wasm-crypto:js",
|
||||
"test:quantus-crypto:js": "./scripts/test-quantus-js.sh",
|
||||
"test:quantus-crypto:rust": "cd packages/quantus-crypto && RUST_BACKTRACE=full cargo test --release",
|
||||
"test:wasm-crypto:deno": "deno test --allow-read --import-map=import_map.json packages/wasm-crypto/test/deno.ts",
|
||||
"test:wasm-crypto:js": "yarn test:wasm-crypto:js:jest && yarn test:wasm-crypto:js:node",
|
||||
"test:wasm-crypto:js:jest": "polkadot-dev-run-test --env node --loader ./packages/wasm-crypto/test/loader-build.js",
|
||||
|
||||
6
packages/quantus-crypto/src/cjs/bytes.d.ts
vendored
6
packages/quantus-crypto/src/cjs/bytes.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export declare const lenIn: number;
|
||||
export declare const lenOut: number;
|
||||
export declare const bytes: string;
|
||||
@@ -1,10 +0,0 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Generated as part of the build, do not edit
|
||||
|
||||
exports.lenIn = 0;
|
||||
|
||||
exports.lenOut = 0;
|
||||
|
||||
exports.bytes = '';
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { base64Decode, unzlibSync } from '@polkadot/wasm-util';
|
||||
// Deep imports, not the package index. `@polkadot/wasm-util`'s index re-exports
|
||||
// `packageDetect`, whose only job is a side effect registering the package with
|
||||
// `@polkadot/util` — a peer dependency we would otherwise inherit for nothing.
|
||||
// These two subpaths are pure functions with no dependencies at all.
|
||||
import { base64Decode } from '@polkadot/wasm-util/base64';
|
||||
import { unzlibSync } from '@polkadot/wasm-util/fflate';
|
||||
|
||||
import { bytes, lenOut } from './bytes.js';
|
||||
import { initSync } from './generated/quantus_crypto.js';
|
||||
|
||||
56
packages/quantus-crypto/test/consumer.mjs
Normal file
56
packages/quantus-crypto/test/consumer.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Consumes the *built* package exactly as quantus/common will — a plain import of
|
||||
// build output, nothing reaching into src or poking the wasm by hand. Run after
|
||||
// ./scripts/build-quantus.sh.
|
||||
//
|
||||
// This exists because the unit tests in src/rs/tests.rs cannot catch packaging
|
||||
// faults. A wasm that is valid before `wasm-opt` and broken after it passes every
|
||||
// cargo test and fails here, which is exactly how binaryen 105's mishandling of
|
||||
// externref tables was found.
|
||||
|
||||
import {
|
||||
accountFromPublicKey, contextForSpec, EXTRINSIC_MIN_SPEC, initWasm,
|
||||
isReady, keypairFromMnemonic, keypairFromSeed, Scheme, SCHEME_NAME,
|
||||
sign, signatureWithPublicKey, sizes, verify
|
||||
} from '@quantus/crypto';
|
||||
|
||||
let fail = 0;
|
||||
const eq = (l, g, w) => { const ok = String(g) === String(w); if (!ok) fail++;
|
||||
console.log(`${ok ? 'PASS' : 'FAIL'} ${l}`); if (!ok) console.log(` got ${g}\n want ${w}`); };
|
||||
|
||||
eq('initWasm() returns no error', initWasm(), 'null');
|
||||
eq('isReady()', isReady(), true);
|
||||
|
||||
const s65 = sizes(Scheme.MlDsa65);
|
||||
eq('ML-DSA-65 sizes', JSON.stringify(s65), '{"publicKey":1952,"secretKey":4032,"signature":3309,"signatureWithPublicKey":5261}');
|
||||
eq('scheme name', SCHEME_NAME[Scheme.MlDsa65], 'ml-dsa-65');
|
||||
eq('variant byte is the enum value', Scheme.MlDsa87, 0);
|
||||
|
||||
// crystal_alice, via the public API only
|
||||
const pair87 = keypairFromSeed(new Uint8Array(32), Scheme.MlDsa87);
|
||||
eq('crystal_alice account id',
|
||||
Buffer.from(accountFromPublicKey(pair87.publicKey)).toString('hex'),
|
||||
'1883df2ae47d1fd428a6b8237ad7b59cf0facccaacac4541ef7758be44b3c333');
|
||||
|
||||
// HD derivation, dev phrase, ML-DSA-65 default path
|
||||
const DEV = 'bottom drive obey lake curtain smoke basket hold race lonely fit walk';
|
||||
const hd = keypairFromMnemonic(DEV, '', "m/44'/189189'/0'/0'/1'", Scheme.MlDsa65);
|
||||
eq('dev phrase account id (ML-DSA-65)',
|
||||
Buffer.from(accountFromPublicKey(hd.publicKey)).toString('hex'),
|
||||
'f647dbdefebcfcf726ba078a83481ffc6f4f33004fdfb4cedacf5a5391bc8f00');
|
||||
|
||||
// the signing-context boundary
|
||||
const msg = new TextEncoder().encode('extrinsic payload');
|
||||
const ctx = contextForSpec(EXTRINSIC_MIN_SPEC);
|
||||
const sig = sign(msg, hd, ctx, Scheme.MlDsa65);
|
||||
eq('signature length', sig.length, s65.signature);
|
||||
eq('verifies at spec 148', verify(msg, sig, hd.publicKey, ctx, Scheme.MlDsa65), true);
|
||||
eq('does NOT verify at spec 147', verify(msg, sig, hd.publicKey, contextForSpec(147), Scheme.MlDsa65), false);
|
||||
eq('contextForSpec(147) is empty', contextForSpec(147).length, 0);
|
||||
|
||||
// the wire form
|
||||
eq('sig || pk length', signatureWithPublicKey(sig, hd.publicKey).length, s65.signatureWithPublicKey);
|
||||
|
||||
process.exit(fail ? 1 : 0);
|
||||
26
packages/quantus-crypto/tsconfig.build.json
Normal file
26
packages/quantus-crypto/tsconfig.build.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "..",
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src",
|
||||
"paths": {
|
||||
"@polkadot/wasm-util/base64": [
|
||||
"wasm-util/build/base64.d.ts"
|
||||
],
|
||||
"@polkadot/wasm-util/fflate": [
|
||||
"wasm-util/build/fflate.d.ts"
|
||||
]
|
||||
},
|
||||
"emitDeclarationOnly": false
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"references": []
|
||||
}
|
||||
58
scripts/build-quantus-js.sh
Executable file
58
scripts/build-quantus-js.sh
Executable file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2026 @quantus/crypto authors & contributors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Builds the JS side of packages/quantus-crypto.
|
||||
#
|
||||
# Not `polkadot-dev-build-ts`, because that tool returns early for any package
|
||||
# not named `@polkadot/*` — twice, in `buildJs` and when collecting `locals` for
|
||||
# import rewriting. Renaming the package into someone else's scope to satisfy a
|
||||
# string check would be worse than not using the tool.
|
||||
#
|
||||
# Nothing is lost by that: this package needs none of what the tool adds (a deno
|
||||
# variant, a rollup bundle, cross-package import rewriting, generated exports
|
||||
# maps). It is four TypeScript files and two generated artifacts. Keeping it out
|
||||
# also means `yarn build:js` stays byte-identical to upstream's behaviour, which
|
||||
# is the rebasability convention in quantus/extension#1.
|
||||
#
|
||||
# ESM only. The consumers — quantus/common and the extension — are ESM, and the
|
||||
# wasm-bindgen glue is ESM-only, so a CJS variant would mean either a second
|
||||
# generated glue or hand-written marshalling. See quantus/wasm#1 before adding one.
|
||||
|
||||
set -e
|
||||
|
||||
PKG=packages/quantus-crypto
|
||||
CRATE=quantus_crypto
|
||||
|
||||
echo "*** Building @quantus/crypto"
|
||||
|
||||
rm -rf $PKG/build-tsc
|
||||
yarn polkadot-exec-tsc --outDir $PKG/build-tsc --project $PKG/tsconfig.build.json
|
||||
|
||||
# tsc emits only what it compiles; the generated glue and the packed bytes are
|
||||
# .js and have to be carried over by hand.
|
||||
mkdir -p $PKG/build/generated
|
||||
cp -r $PKG/build-tsc/* $PKG/build/
|
||||
cp $PKG/src/generated/$CRATE.js $PKG/build/generated/$CRATE.js
|
||||
cp $PKG/src/generated/$CRATE.d.ts $PKG/build/generated/$CRATE.d.ts
|
||||
rm -rf $PKG/build-tsc
|
||||
|
||||
# package.json, with the paths rewritten for a consumer installing the build
|
||||
# output rather than the source tree.
|
||||
node -e "
|
||||
const fs = require('node:fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('$PKG/package.json', 'utf-8'));
|
||||
|
||||
delete pkg.private;
|
||||
pkg.main = './index.js';
|
||||
pkg.types = './index.d.ts';
|
||||
pkg.exports = {
|
||||
'.': { types: './index.d.ts', default: './index.js' },
|
||||
'./package.json': './package.json'
|
||||
};
|
||||
|
||||
fs.writeFileSync('$PKG/build/package.json', JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
cp $PKG/README.md $PKG/build/README.md
|
||||
|
||||
echo "*** Built $(ls $PKG/build/*.js | wc -l) modules"
|
||||
@@ -32,8 +32,24 @@ echo "*** Converting to WASM"
|
||||
--out-dir packages/$PKG/build-wasm \
|
||||
--target web
|
||||
|
||||
# The glue is a build artifact but is checked in, so the package can be built
|
||||
# without a Rust toolchain — the same reasoning as upstream checking in an empty
|
||||
# bytes.js. Copy it back so the two never drift.
|
||||
echo "*** Updating checked-in bindings"
|
||||
cp packages/$PKG/build-wasm/$CRATE.js packages/$PKG/src/generated/$CRATE.js
|
||||
cp packages/$PKG/build-wasm/$CRATE.d.ts packages/$PKG/src/generated/$CRATE.d.ts
|
||||
|
||||
# binaryen-quantus, not binaryen. Upstream pins version_105 (2021), which predates
|
||||
# the externref tables wasm-bindgen 0.2.128 emits: it "optimises" the table into
|
||||
# something that fails at instantiation with
|
||||
# `WebAssembly.Table.grow(): failed to grow table by 4`. The wasm is valid before
|
||||
# wasm-opt and broken after, and nothing in the build says so — it only surfaces
|
||||
# when a consumer tries to init. Same shape of problem as the two bindgens.
|
||||
echo "*** Optimising WASM output"
|
||||
./binaryen/bin/wasm-opt $WASM -Oz -o $OPT
|
||||
./binaryen-quantus/bin/wasm-opt $WASM -Oz -o $OPT
|
||||
|
||||
# Must come before packing: tsc clears build/, which is where bytes.js lands.
|
||||
./scripts/build-quantus-js.sh
|
||||
|
||||
echo "*** Packing WASM into baseX"
|
||||
PKG_NAME=$PKG CRATE_NAME=$CRATE node ./scripts/pack-quantus-base.mjs
|
||||
|
||||
@@ -74,3 +74,15 @@ if [ ! -d "bindgen-quantus" ]; then
|
||||
curl -L $BINDGEN_REPO/releases/download/$QUANTUS_BINDGEN_VER/$QUANTUS_BINDGEN_ZIP.tar.gz | tar xz
|
||||
mv $QUANTUS_BINDGEN_ZIP bindgen-quantus
|
||||
fi
|
||||
|
||||
# Quantus: a second binaryen, for the same reason as the second wasm-bindgen.
|
||||
# version_105 above cannot handle externref tables and silently produces a wasm
|
||||
# that fails to instantiate. See quantus/wasm#1.
|
||||
QUANTUS_BINARYEN_VER=version_123
|
||||
QUANTUS_BINARYEN_ZIP=binaryen-$QUANTUS_BINARYEN_VER-x86_64-linux
|
||||
|
||||
if [ ! -d "binaryen-quantus" ]; then
|
||||
echo "*** Downloading binaryen for quantus-crypto"
|
||||
curl -L $BINARYEN_REPO/releases/download/$QUANTUS_BINARYEN_VER/$QUANTUS_BINARYEN_ZIP.tar.gz | tar xz
|
||||
mv binaryen-$QUANTUS_BINARYEN_VER binaryen-quantus
|
||||
fi
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
// A near-copy of pack-wasm-base.mjs. Kept separate rather than parameterised
|
||||
// because that script is upstream's and rebases should not have to reconcile our
|
||||
// changes with theirs — the duplication is the cheaper of the two costs. It also
|
||||
// emits no deno variant, which we do not ship.
|
||||
// changes with theirs — the duplication is the cheaper of the two costs.
|
||||
//
|
||||
// ESM only: no deno variant and no CJS variant, matching what the package ships.
|
||||
// This must run *after* the TypeScript build, which clears build/.
|
||||
|
||||
import fs from 'node:fs';
|
||||
|
||||
@@ -12,7 +14,6 @@ import { zlibSync } from 'fflate/node';
|
||||
|
||||
const PKG_NAME = process.env['PKG_NAME'];
|
||||
const CRATE_NAME = process.env['CRATE_NAME'];
|
||||
const DIR_CJS = `./packages/${PKG_NAME}/build/cjs`;
|
||||
const DIR_ESM = `./packages/${PKG_NAME}/build`;
|
||||
const HDR = `// Copyright 2026 @quantus/crypto authors & contributors\n// SPDX-License-Identifier: Apache-2.0\n\n// Generated as part of the build, do not edit\n`;
|
||||
|
||||
@@ -22,19 +23,7 @@ const base64 = compressed.toString('base64');
|
||||
|
||||
console.log(`*** Compressed WASM: in=${data.length}, out=${compressed.length}, opt=${(100 * compressed.length / data.length).toFixed(2)}%, base64=${base64.length}`);
|
||||
|
||||
fs.mkdirSync(DIR_CJS, { recursive: true });
|
||||
|
||||
// Both module systems, as upstream does for wasm-crypto-wasm. The CJS copy sits
|
||||
// under a directory carrying its own `{"type":"commonjs"}`, because the package
|
||||
// itself is `"type": "module"` and node would otherwise refuse to load an
|
||||
// `exports.`-style file from it.
|
||||
fs.writeFileSync(`${DIR_CJS}/bytes.js`, `${HDR}
|
||||
exports.lenIn = ${compressed.length};
|
||||
|
||||
exports.lenOut = ${data.length};
|
||||
|
||||
exports.bytes = '${base64}';
|
||||
`);
|
||||
fs.mkdirSync(DIR_ESM, { recursive: true });
|
||||
|
||||
fs.writeFileSync(`${DIR_ESM}/bytes.js`, `${HDR}
|
||||
export const lenIn = ${compressed.length};
|
||||
|
||||
29
scripts/test-quantus-js.sh
Executable file
29
scripts/test-quantus-js.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2026 @quantus/crypto authors & contributors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Runs the consumer test against a staged node_modules rather than in-place.
|
||||
#
|
||||
# In this repo `node_modules/@polkadot/wasm-util` symlinks to the package *source*
|
||||
# directory, whose package.json carries no `exports` map — only the build output
|
||||
# does. So a deep import like `@polkadot/wasm-util/base64` resolves for a real
|
||||
# consumer and fails here, which would make the in-place test wrong in a way that
|
||||
# has nothing to do with our package. Staging the built artifacts the way an
|
||||
# install lays them out tests module resolution too, which is half of what can
|
||||
# break in a published package.
|
||||
|
||||
set -e
|
||||
|
||||
PKG=packages/quantus-crypto
|
||||
STAGE=$PKG/build-test
|
||||
|
||||
rm -rf $STAGE
|
||||
mkdir -p $STAGE/node_modules/@quantus $STAGE/node_modules/@polkadot
|
||||
|
||||
cp -r $PKG/build $STAGE/node_modules/@quantus/crypto
|
||||
cp -r packages/wasm-util/build $STAGE/node_modules/@polkadot/wasm-util
|
||||
echo '{ "name": "quantus-crypto-consumer-test", "type": "module", "version": "0.0.0" }' > $STAGE/package.json
|
||||
cp $PKG/test/consumer.mjs $STAGE/consumer.mjs
|
||||
|
||||
node $STAGE/consumer.mjs
|
||||
rm -rf $STAGE
|
||||
@@ -610,6 +610,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@quantus/crypto@workspace:packages/quantus-crypto":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@quantus/crypto@workspace:packages/quantus-crypto"
|
||||
dependencies:
|
||||
"@polkadot/wasm-util": "npm:7.5.4"
|
||||
tslib: "npm:^2.7.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@rollup/plugin-alias@npm:^5.1.1":
|
||||
version: 5.1.1
|
||||
resolution: "@rollup/plugin-alias@npm:5.1.1"
|
||||
|
||||
Reference in New Issue
Block a user