Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
dc295dc3f5
|
|||
|
4e5b479db4
|
|||
|
d61e886359
|
|||
|
0b1cfe35b3
|
|||
|
09a96b64bf
|
|||
|
e6ff57a334
|
|||
|
f1c51661df
|
|||
|
3a1611a92e
|
|||
|
|
e8bf9e20c7
|
||
|
|
a6d3685c59
|
||
|
|
8323e442d9
|
||
|
|
b882f914e7
|
||
|
|
02a3f004d7
|
||
|
|
1f1f5729ab
|
||
|
|
6eb04f63ac
|
||
|
|
65286fb3ec | ||
|
|
f55a3e75a2 | ||
|
|
6c8b0afd8d | ||
|
|
9c6611087c | ||
|
|
dc34df3b54 | ||
|
|
8bf7e88458 | ||
|
|
e8f1fb3aed | ||
|
|
ba6155880e | ||
|
|
1e696a5520 | ||
|
|
b6c704eca8 | ||
|
|
c7053fa112 | ||
|
|
d76ab093a8 | ||
|
|
4a54a37ae1 | ||
|
|
0eb433f3c2 | ||
|
|
278ecc7210 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,7 +1,10 @@
|
||||
binaryen/
|
||||
binaryen-quantus/
|
||||
bindgen/
|
||||
bindgen-quantus/
|
||||
build/
|
||||
build-*/
|
||||
build-test/
|
||||
bytes/
|
||||
coverage/
|
||||
node_modules/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017-2025 @polkadot/wasm-crypto authors & contributors
|
||||
// Copyright 2017-2026 @polkadot/wasm-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
module.exports = require('@polkadot/dev/config/prettier.cjs');
|
||||
|
||||
22
CHANGELOG.md
22
CHANGELOG.md
@@ -1,5 +1,27 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 7.5.4 Dec 9, 2025
|
||||
|
||||
Changes:
|
||||
|
||||
- Bump polkadot-js dependencies ([#605](https://github.com/polkadot-js/wasm/pull/605))
|
||||
|
||||
|
||||
## 7.5.3 Nov 24, 2025
|
||||
|
||||
Changes:
|
||||
|
||||
- Bump @polkadot dependencies ([#603](https://github.com/polkadot-js/wasm/pull/603))
|
||||
|
||||
|
||||
## 7.5.2 Nov 10, 2025
|
||||
|
||||
Changes:
|
||||
|
||||
- Fix/Revert asm build ([#599](https://github.com/polkadot-js/wasm/pull/599))
|
||||
- Bump @polkadot dependencies ([#601](https://github.com/polkadot-js/wasm/pull/601))
|
||||
|
||||
|
||||
## 7.5.1 aug 25, 2025
|
||||
|
||||
Changes:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
618 Jaco 2024 (#559)
|
||||
19 Tarik Gul Bump dev to 0.83.2 (#578)
|
||||
12 Valentin Fernandez 7.5.1 (#596)
|
||||
3 rajk93 Added validation checks in PBKDF2 and Scrypt hashing functions (#584)
|
||||
10 rajk93 7.5.4 (#606)
|
||||
1 Evgeny Fixed type (#121)
|
||||
1 Shunfan Zhou Support sr25519 agreement (#209)
|
||||
1 Steve Degosserie Expose Schnorrkel's VRF capabilities (#170)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017-2025 @polkadot/wasm-crypto authors & contributors
|
||||
// Copyright 2017-2026 @polkadot/wasm-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import baseConfig from '@polkadot/dev/config/eslint';
|
||||
|
||||
16
package.json
16
package.json
@@ -14,10 +14,10 @@
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "7.5.1",
|
||||
"version": "7.5.4",
|
||||
"versions": {
|
||||
"git": "7.5.1",
|
||||
"npm": "7.5.1"
|
||||
"git": "7.5.4",
|
||||
"npm": "7.5.4"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -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,17 +36,19 @@
|
||||
"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",
|
||||
"test:wasm-crypto:js:node": "node --no-warnings --loader ./packages/wasm-crypto/test/loader-build.js ./packages/wasm-crypto/test/wasm.js",
|
||||
"test:wasm-crypto:js:node": "node --no-warnings --loader ./packages/wasm-crypto/test/loader-build.js ./packages/wasm-crypto/test/wasm.js && node --no-warnings --loader ./packages/wasm-crypto/test/loader-build.js ./packages/wasm-crypto/test/asm.js",
|
||||
"test:wasm-crypto:rust": "cd packages/wasm-crypto && RUST_BACKTRACE=full cargo test --release -- --test-threads=1 --nocapture"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@polkadot/dev": "^0.83.3",
|
||||
"@polkadot/util": "^13.5.5",
|
||||
"@polkadot/util": "^14.0.1",
|
||||
"@types/node": "^20.16.1",
|
||||
"fflate": "^0.8.2"
|
||||
},
|
||||
|
||||
602
packages/quantus-codec/Cargo.lock
generated
Normal file
602
packages/quantus-codec/Cargo.lock
generated
Normal file
@@ -0,0 +1,602 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||
dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bs58"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "byte-slice-cast"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "const_format"
|
||||
version = "0.2.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e"
|
||||
dependencies = [
|
||||
"const_format_proc_macros",
|
||||
"konst",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const_format_proc_macros"
|
||||
version = "0.2.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "frame-metadata"
|
||||
version = "23.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ba5be0edbdb824843a0f9c6f0906ecfc66c5316218d74457003218b24909ed0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "impl-trait-for-tuples"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "konst"
|
||||
version = "0.2.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb"
|
||||
dependencies = [
|
||||
"konst_macro_rules",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "konst_macro_rules"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "parity-scale-codec"
|
||||
version = "3.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"byte-slice-cast",
|
||||
"const_format",
|
||||
"impl-trait-for-tuples",
|
||||
"parity-scale-codec-derive",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parity-scale-codec-derive"
|
||||
version = "3.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quantus_codec"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"blake2",
|
||||
"bs58",
|
||||
"frame-metadata",
|
||||
"parity-scale-codec",
|
||||
"scale-decode",
|
||||
"scale-info",
|
||||
"scale-value",
|
||||
"serde_json",
|
||||
"twox-hash",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "scale-bits"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27243ab0d2d6235072b017839c5f0cd1a3b1ce45c0f7a715363b0c7d36c76c94"
|
||||
dependencies = [
|
||||
"parity-scale-codec",
|
||||
"scale-info",
|
||||
"scale-type-resolver",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scale-decode"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d6ed61699ad4d54101ab5a817169259b5b0efc08152f8632e61482d8a27ca3d"
|
||||
dependencies = [
|
||||
"parity-scale-codec",
|
||||
"scale-bits",
|
||||
"scale-type-resolver",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scale-encode"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2a976d73564a59e482b74fd5d95f7518b79ca8c8ca5865398a4d629dd15ee50"
|
||||
dependencies = [
|
||||
"parity-scale-codec",
|
||||
"scale-bits",
|
||||
"scale-type-resolver",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scale-info"
|
||||
version = "2.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"derive_more",
|
||||
"parity-scale-codec",
|
||||
"scale-info-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scale-info-derive"
|
||||
version = "2.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scale-type-resolver"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0cded6518aa0bd6c1be2b88ac81bf7044992f0f154bfbabd5ad34f43512abcb"
|
||||
dependencies = [
|
||||
"scale-info",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scale-value"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3b64809a541e8d5a59f7a9d67cc700cdf5d7f907932a83a0afdedc90db07ccb"
|
||||
dependencies = [
|
||||
"either",
|
||||
"parity-scale-codec",
|
||||
"scale-bits",
|
||||
"scale-decode",
|
||||
"scale-encode",
|
||||
"scale-type-resolver",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.229"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.151"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.13+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "twox-hash"
|
||||
version = "2.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5283634e518fe9e82c7b20520bb4bc209009fd16c82077c802f8111ecbb0117a"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
63
packages/quantus-codec/Cargo.toml
Normal file
63
packages/quantus-codec/Cargo.toml
Normal file
@@ -0,0 +1,63 @@
|
||||
# Metadata-driven SCALE encode/decode for the Quantus chain, compiled to WASM.
|
||||
#
|
||||
# A separate crate from `quantus-crypto` for the same reason that one is separate
|
||||
# from `wasm-crypto`: different dependency graphs, built independently. They ship
|
||||
# as sibling packages and the extension uses both — this one decides *what bytes*
|
||||
# get signed, that one signs them.
|
||||
#
|
||||
# Why this exists at all rather than `@polkadot/api`: quantus/api#1. In short,
|
||||
# polkadot-js cannot decode a Quantus block (it reads the extrinsic preamble byte
|
||||
# as a version when the top two bits are a type tag), it refuses fixed arrays
|
||||
# longer than 2048 (ML-DSA signatures are 5261 and 7219 bytes), and — the part
|
||||
# that matters after those are patched — it *guesses* that signed extensions it
|
||||
# does not recognise contribute nothing to the signed payload. On a chain whose
|
||||
# encoding has already changed between runtimes, a guess like that produces a
|
||||
# valid signature over the wrong bytes, which arrives as `BadProof` and looks
|
||||
# exactly like a wrong key. See quantus/wasm#3.
|
||||
|
||||
[package]
|
||||
authors = ["Quantus Network Developers <hello@quantus.com>"]
|
||||
description = "Metadata-driven SCALE codec for the Quantus chain, as WASM bindings."
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
name = "quantus_codec"
|
||||
publish = false
|
||||
repository = "https://git.lair.cafe/quantus/wasm"
|
||||
resolver = "2"
|
||||
version = "0.0.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
# Versions match blackbeard.observer's, which is the other consumer decoding this
|
||||
# chain against its own metadata and the reference implementation for this crate.
|
||||
frame-metadata = { version = "23", default-features = false, features = ["current", "decode"] }
|
||||
parity-scale-codec = { version = "3", default-features = false, features = ["derive"] }
|
||||
scale-info = { version = "2", default-features = false }
|
||||
scale-value = { version = "0.18", default-features = false }
|
||||
# Only for `IgnoreVisitor`. `scale_value` sizes a sequence's Vec from the length
|
||||
# prefix *before* decoding an item, so a blob that disagrees with the registry can
|
||||
# ask for an allocation of any size and abort the process — there is no Err to
|
||||
# catch. Pinned to the version `scale-value` itself resolves so both see one
|
||||
# registry. blackbeard.observer took a 76 GiB allocation to find this.
|
||||
scale-decode = { version = "0.16", default-features = false }
|
||||
serde_json = "1"
|
||||
wasm-bindgen = "0.2"
|
||||
# Storage keys. Substrate hashes a pallet prefix and an item name with twox128
|
||||
# and each map key with whatever hasher the metadata declares for it.
|
||||
blake2 = { version = "0.10", default-features = false }
|
||||
twox-hash = { version = "2", default-features = false, features = ["xxhash64"] }
|
||||
# SS58. An account id rendered as 32 bytes of hex is a correct description of the
|
||||
# value and unreadable to the person being asked to approve it.
|
||||
bs58 = { version = "0.5", default-features = false, features = ["alloc"] }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
debug = false
|
||||
debug-assertions = false
|
||||
incremental = false
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
panic = "abort"
|
||||
rpath = false
|
||||
88
packages/quantus-codec/README.md
Normal file
88
packages/quantus-codec/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# @quantus/codec
|
||||
|
||||
Metadata-driven SCALE encode and decode for the [Quantus](https://quantus.com)
|
||||
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](https://git.lair.cafe/quantus/api/issues/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
|
||||
|
||||
```ts
|
||||
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](https://git.lair.cafe/quantus/wasm/issues/1) and
|
||||
[#3](https://git.lair.cafe/quantus/wasm/issues/3).
|
||||
24
packages/quantus-codec/package.json
Normal file
24
packages/quantus-codec/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"author": "Quantus Network Developers <hello@quantus.com>",
|
||||
"bugs": "https://git.lair.cafe/quantus/wasm/issues",
|
||||
"description": "Metadata-driven SCALE encode/decode for the Quantus chain",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"homepage": "https://git.lair.cafe/quantus/wasm/src/branch/main/packages/quantus-codec#readme",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@quantus/codec",
|
||||
"repository": {
|
||||
"directory": "packages/quantus-codec",
|
||||
"type": "git",
|
||||
"url": "https://git.lair.cafe/quantus/wasm.git"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "0.5.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"fflate": "^0.8.2",
|
||||
"tslib": "^2.7.0"
|
||||
}
|
||||
}
|
||||
8
packages/quantus-codec/rust-toolchain.toml
Normal file
8
packages/quantus-codec/rust-toolchain.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
# Matches the chain's toolchain (chain/rust-toolchain), so this crate is built by
|
||||
# the same compiler that builds the runtime it has to agree with. Upstream's
|
||||
# `wasm-crypto` keeps its own nightly-2022-06-24 pin; the two builds are separate
|
||||
# on purpose. See quantus/wasm#1.
|
||||
[toolchain]
|
||||
channel = "1.93.0"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
profile = "minimal"
|
||||
51
packages/quantus-codec/src/base64.ts
Normal file
51
packages/quantus-codec/src/base64.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
|
||||
// An array indexer rather than a Map: the input is ASCII by construction, so it
|
||||
// cannot overflow, and array access is measurably faster on the hot loop.
|
||||
const MAP = new Array<number>(256);
|
||||
|
||||
for (let i = 0; i < CHARS.length; i++) {
|
||||
MAP[CHARS.charCodeAt(i)] = i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode base64 into a caller-supplied buffer.
|
||||
*
|
||||
* Deliberately not `atob` or `Buffer.from`: the first is browser-only, the second
|
||||
* node-only, and this runs in an MV3 service worker, a Worker, node tests and a
|
||||
* bundled extension page. The output length is known at build time, so the
|
||||
* caller provides the buffer and there is no growth or reallocation.
|
||||
*
|
||||
* This is a reimplementation of `@polkadot/wasm-util`'s base64Decode, which was
|
||||
* the dependency it replaced. That package's index re-exports `packageDetect`,
|
||||
* dragging in a `@polkadot/util` peer dependency for a side effect we do not
|
||||
* want, and being a workspace package it resolved through its own repo's
|
||||
* node_modules when consumed by symlink from another checkout. Fifteen lines is
|
||||
* cheaper than either problem.
|
||||
*/
|
||||
export function base64Decode (data: string, out: Uint8Array): Uint8Array {
|
||||
let byte = 0;
|
||||
let bits = 0;
|
||||
let pos = 0;
|
||||
|
||||
for (let i = 0; i < data.length && pos < out.length; i++) {
|
||||
const value = MAP[data.charCodeAt(i)];
|
||||
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte = (byte << 6) | value;
|
||||
bits += 6;
|
||||
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
out[pos++] = (byte >>> bits) & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
6
packages/quantus-codec/src/bytes.d.ts
vendored
Normal file
6
packages/quantus-codec/src/bytes.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
// 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;
|
||||
10
packages/quantus-codec/src/bytes.js
Normal file
10
packages/quantus-codec/src/bytes.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Generated as part of the build, do not edit
|
||||
|
||||
export const lenIn = 0;
|
||||
|
||||
export const lenOut = 0;
|
||||
|
||||
export const bytes = '';
|
||||
242
packages/quantus-codec/src/codec.ts
Normal file
242
packages/quantus-codec/src/codec.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
// Copyright 2026 @quantus/codec authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { initWasm } from './init.js';
|
||||
|
||||
import { QuantusRuntime } from './generated/quantus_codec.js';
|
||||
|
||||
/** What one signed extension needs from the caller, as the runtime declares it. */
|
||||
export interface ExtensionNeed {
|
||||
identifier: string;
|
||||
/** Whether its `ty` encodes to anything — i.e. whether it goes on the wire. */
|
||||
needsExtra: boolean;
|
||||
/** Whether its `additional_signed` encodes to anything. */
|
||||
needsAdditional: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A value for one signed extension. Omit a half the runtime declares as empty.
|
||||
*
|
||||
* Each half is either interpreted against the type the runtime declares
|
||||
* (`extra`, `additional`) or supplied already SCALE-encoded (`extraHex`,
|
||||
* `additionalHex`). Pre-encoded bytes are **validated by round trip**, not
|
||||
* trusted: they are decoded against the declared type and re-encoded, and
|
||||
* anything that does not come back identical is refused rather than signed.
|
||||
* Setting both halves of a pair is a contradiction and is also refused.
|
||||
*/
|
||||
export interface ExtensionValue {
|
||||
extra?: unknown;
|
||||
extraHex?: string;
|
||||
additional?: unknown;
|
||||
additionalHex?: string;
|
||||
}
|
||||
|
||||
export type ExtensionValues = Record<string, ExtensionValue>;
|
||||
|
||||
export interface StorageTarget {
|
||||
/** The full key, ready for `state_getStorage`. */
|
||||
key: string;
|
||||
/** The registry type its value decodes as — pass to `decodeStorage`. */
|
||||
valueTy: number;
|
||||
/**
|
||||
* What the chain means when `state_getStorage` returns nothing.
|
||||
*
|
||||
* Hex for a `Default` entry — an unfunded account reads as a zero balance —
|
||||
* and `null` for an `Optional` one, where nothing means nothing. A wallet that
|
||||
* conflated the two would report a failure for an account that simply has no
|
||||
* money in it.
|
||||
*/
|
||||
default: string | null;
|
||||
}
|
||||
|
||||
export interface DecodedExtrinsic {
|
||||
/** The preamble byte's low six bits — **not** the byte. See `decodeExtrinsic`. */
|
||||
version: number;
|
||||
signed: boolean;
|
||||
address: unknown;
|
||||
signature: unknown;
|
||||
extra: unknown;
|
||||
call: unknown;
|
||||
}
|
||||
|
||||
/** Everything needed to fill in the signed extensions Substrate itself defines. */
|
||||
export interface PayloadOptions {
|
||||
specVersion: number;
|
||||
transactionVersion: number;
|
||||
genesisHash: string;
|
||||
/** The era's birth block. For an immortal era this is the genesis hash. */
|
||||
blockHash: string;
|
||||
nonce: number;
|
||||
tip?: bigint | string;
|
||||
/** `'Immortal'`, or `{ MortalN: phase }` as the registry spells it. */
|
||||
era?: unknown;
|
||||
/**
|
||||
* The era already SCALE-encoded — what a dapp hands a wallet, since it did the
|
||||
* encoding itself and the wallet has no way to render those two bytes as a
|
||||
* variant without knowing the era algorithm. Takes precedence over `era`, and
|
||||
* is round-tripped against the runtime's own `Era` type before it is used.
|
||||
*/
|
||||
eraHex?: string;
|
||||
/** `CheckMetadataHash`: `null` disables it, which is what a wallet wants. */
|
||||
metadataHash?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A runtime, loaded from the metadata it produced about itself.
|
||||
*
|
||||
* Construct one per spec version and keep it: parsing metadata is the expensive
|
||||
* part, and the blob does not change until the chain upgrades.
|
||||
*/
|
||||
export class Runtime {
|
||||
readonly #inner: QuantusRuntime;
|
||||
|
||||
private constructor (inner: QuantusRuntime) {
|
||||
this.#inner = inner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse metadata exactly as `state_getMetadata` returns it.
|
||||
*
|
||||
* That RPC takes a block hash and makes the node run `Metadata_metadata`
|
||||
* against the runtime code in *that block's* state — so this is the runtime
|
||||
* describing itself, and it is the only description that cannot go stale.
|
||||
*/
|
||||
static fromMetadata (metadata: Uint8Array): Runtime {
|
||||
const failed = initWasm();
|
||||
|
||||
if (failed) {
|
||||
throw new Error(`@quantus/codec: WASM unavailable: ${failed}`);
|
||||
}
|
||||
|
||||
return new Runtime(new QuantusRuntime(metadata));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render account ids as SS58 at this prefix when decoding.
|
||||
*
|
||||
* Off until set. The prefix belongs to the chain a caller is talking to, not
|
||||
* to the metadata, so this is not something the package can infer — and a
|
||||
* guessed one would put a plausible, wrong address in front of somebody about
|
||||
* to approve a transfer. Account ids are found by their **registry path**, so
|
||||
* a block hash, which is also 32 bytes, still renders as hex.
|
||||
*/
|
||||
setSs58Format (prefix: number): void {
|
||||
this.#inner.setSs58Format(prefix);
|
||||
}
|
||||
|
||||
/** The extrinsic format version the metadata declares. */
|
||||
get extrinsicVersion (): number {
|
||||
return this.#inner.extrinsicVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Every signed extension, in the order the runtime applies them — which is the
|
||||
* order their bytes appear in the payload.
|
||||
*/
|
||||
signedExtensions (): ExtensionNeed[] {
|
||||
return JSON.parse(this.#inner.signedExtensions()) as ExtensionNeed[];
|
||||
}
|
||||
|
||||
/** Encode a call by name. `args` is keyed by the runtime's own argument names. */
|
||||
encodeCall (pallet: string, call: string, args: Record<string, unknown>): Uint8Array {
|
||||
return this.#inner.encodeCall(pallet, call, JSON.stringify(args));
|
||||
}
|
||||
|
||||
/** The `extra`: what the extensions contribute to the extrinsic itself. */
|
||||
encodeExtra (values: ExtensionValues): Uint8Array {
|
||||
return this.#inner.encodeExtra(JSON.stringify(values));
|
||||
}
|
||||
|
||||
/**
|
||||
* The bytes to sign: `call ‖ extra ‖ additional`.
|
||||
*
|
||||
* Substrate's rule that a payload over 256 bytes is signed as its BLAKE2b-256
|
||||
* hash is **not** applied here — that belongs with the signing code, which also
|
||||
* chooses the FIPS 204 context. Splitting one rule across two packages is how
|
||||
* the halves drift apart.
|
||||
*/
|
||||
signerPayload (call: Uint8Array, values: ExtensionValues): Uint8Array {
|
||||
return this.#inner.signerPayload(call, JSON.stringify(values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble a signed extrinsic, ready for `author_submitAndWatchExtrinsic`.
|
||||
*
|
||||
* `signature` is the encoded `Signature` type with its variant byte already in
|
||||
* place: the signer knows which ML-DSA scheme its key is, and re-deriving that
|
||||
* here from the byte length would be a second source of truth.
|
||||
*/
|
||||
encodeExtrinsic (address: unknown, signature: Uint8Array, extra: Uint8Array, call: Uint8Array): Uint8Array {
|
||||
return this.#inner.encodeExtrinsic(JSON.stringify(address), signature, extra, call);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode one extrinsic, length prefix and all.
|
||||
*
|
||||
* The returned `version` is the preamble byte's low six bits. The top two are a
|
||||
* type tag — `0b00` bare, `0b10` signed, `0b01` general — so Quantus emits
|
||||
* `0x84` (signed, v4) and `0x05` (bare, v5) in the same block while the
|
||||
* metadata declares version 4. Three numbers, all correct. Reading that byte as
|
||||
* a version is why `@polkadot/api` cannot decode a single block of this chain.
|
||||
*/
|
||||
decodeExtrinsic (blob: Uint8Array): DecodedExtrinsic {
|
||||
return JSON.parse(this.#inner.decodeExtrinsic(blob)) as DecodedExtrinsic;
|
||||
}
|
||||
|
||||
/** Decode a bare call — what an approval screen shows the user. */
|
||||
decodeCall (bytes: Uint8Array): unknown {
|
||||
return JSON.parse(this.#inner.decodeCall(bytes)) as unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a storage value lives, and what it decodes as.
|
||||
*
|
||||
* `keys` are interpreted against the key types the runtime declares, so an
|
||||
* `AccountId32` is the hex string its inner array accepts. The hashers come
|
||||
* from the metadata too — nothing here knows that `System::Account` is
|
||||
* `Blake2_128Concat`.
|
||||
*/
|
||||
storageTarget (pallet: string, item: string, keys: unknown[] = []): StorageTarget {
|
||||
return JSON.parse(this.#inner.storageTarget(pallet, item, JSON.stringify(keys))) as StorageTarget;
|
||||
}
|
||||
|
||||
/** Decode a storage value against the `valueTy` that `storageTarget` reported. */
|
||||
decodeStorage (valueTy: number, bytes: Uint8Array): unknown {
|
||||
return JSON.parse(this.#inner.decodeStorage(valueTy, bytes)) as unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill in the signed extensions that Substrate itself defines, from one
|
||||
* options object.
|
||||
*
|
||||
* This covers the extensions whose meaning is fixed by Substrate. It
|
||||
* deliberately does **not** try to cover every extension a runtime might
|
||||
* declare: anything else that needs a value will be refused by
|
||||
* `signerPayload` with the extension's name, which is the correct outcome —
|
||||
* a wallet that cannot sign is a bug report, and one that signs a payload
|
||||
* missing bytes the runtime put there is a `BadProof` nobody can diagnose.
|
||||
*
|
||||
* Pass the result, extended with whatever else this runtime asks for, to
|
||||
* `signerPayload` and `encodeExtra`.
|
||||
*/
|
||||
standardExtensions (options: PayloadOptions): ExtensionValues {
|
||||
const values: ExtensionValues = {
|
||||
ChargeTransactionPayment: { extra: (options.tip ?? 0n).toString() },
|
||||
CheckGenesis: { additional: options.genesisHash },
|
||||
CheckMetadataHash: {
|
||||
// `Mode::Disabled`, and `None`. Enabling it would mean shipping a
|
||||
// metadata hash this package has no way to compute.
|
||||
additional: options.metadataHash ? { Some: options.metadataHash } : 'None',
|
||||
extra: 'Disabled'
|
||||
},
|
||||
CheckMortality: options.eraHex
|
||||
? { additional: options.blockHash, extraHex: options.eraHex }
|
||||
: { additional: options.blockHash, extra: options.era ?? 'Immortal' },
|
||||
CheckNonce: { extra: options.nonce },
|
||||
CheckSpecVersion: { additional: options.specVersion },
|
||||
CheckTxVersion: { additional: options.transactionVersion }
|
||||
};
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
125
packages/quantus-codec/src/generated/quantus_codec.d.ts
vendored
Normal file
125
packages/quantus-codec/src/generated/quantus_codec.d.ts
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* A loaded runtime description, held across calls so the metadata is parsed
|
||||
* once per spec version rather than once per signature.
|
||||
*/
|
||||
export class QuantusRuntime {
|
||||
free(): void;
|
||||
[Symbol.dispose](): void;
|
||||
/**
|
||||
* Decode a bare call — what an approval screen shows the user.
|
||||
*/
|
||||
decodeCall(bytes: Uint8Array): string;
|
||||
/**
|
||||
* Decode one extrinsic as this runtime describes it, as JSON.
|
||||
*/
|
||||
decodeExtrinsic(blob: Uint8Array): string;
|
||||
/**
|
||||
* Decode a storage value against the type id `storageTarget` reported.
|
||||
*/
|
||||
decodeStorage(value_ty: number, bytes: Uint8Array): string;
|
||||
/**
|
||||
* Encode a call by name. `args` is a JSON object keyed by argument name.
|
||||
*/
|
||||
encodeCall(pallet: string, call: string, args: string): Uint8Array;
|
||||
/**
|
||||
* The `extra` alone, which the extrinsic carries and the payload repeats.
|
||||
*/
|
||||
encodeExtra(extensions: string): Uint8Array;
|
||||
/**
|
||||
* Assemble a signed extrinsic, ready for `author_submitAndWatchExtrinsic`.
|
||||
*/
|
||||
encodeExtrinsic(address: string, signature: Uint8Array, extra: Uint8Array, call: Uint8Array): Uint8Array;
|
||||
/**
|
||||
* The extrinsic format version this runtime declares.
|
||||
*/
|
||||
extrinsicVersion(): number;
|
||||
/**
|
||||
* Parse metadata as `state_getMetadata` returns it.
|
||||
*/
|
||||
constructor(metadata: Uint8Array);
|
||||
/**
|
||||
* Render account ids as SS58 at this prefix when decoding.
|
||||
*
|
||||
* Off until set. The prefix is a property of the chain a caller is talking
|
||||
* to rather than of the metadata, and a guessed one would put a plausible,
|
||||
* wrong address in front of somebody about to approve a transfer.
|
||||
*/
|
||||
setSs58Format(prefix: number): void;
|
||||
/**
|
||||
* Every signed extension, in order, as
|
||||
* `[{ identifier, needsExtra, needsAdditional }]`.
|
||||
*
|
||||
* The two booleans are what a caller has to satisfy, read from the
|
||||
* registry. A caller that ignores them gets an error rather than a short
|
||||
* payload.
|
||||
*/
|
||||
signedExtensions(): string;
|
||||
/**
|
||||
* The bytes to sign, given an encoded call and the extension values.
|
||||
*
|
||||
* `extensions` is a JSON object keyed by extension identifier, each value
|
||||
* `{ extra?, additional? }`. Omitting one the runtime declares as non-empty
|
||||
* is an error — see [`Runtime::encode_extensions`].
|
||||
*/
|
||||
signerPayload(call: Uint8Array, extensions: string): Uint8Array;
|
||||
/**
|
||||
* Where a storage value lives, and what it decodes as.
|
||||
*
|
||||
* Returns `{ key, valueTy, default }` — `key` ready for `state_getStorage`,
|
||||
* and `default` the bytes the chain means when it returns nothing, or null
|
||||
* for an entry where nothing means nothing. An unfunded account reads as a
|
||||
* zero balance through the first and as an error through the second, so the
|
||||
* distinction is not a detail.
|
||||
*/
|
||||
storageTarget(pallet: string, item: string, keys: string): string;
|
||||
}
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly __wbg_quantusruntime_free: (a: number, b: number) => void;
|
||||
readonly quantusruntime_decodeCall: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly quantusruntime_decodeExtrinsic: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly quantusruntime_decodeStorage: (a: number, b: number, c: number, d: number) => [number, number, number, number];
|
||||
readonly quantusruntime_encodeCall: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number];
|
||||
readonly quantusruntime_encodeExtra: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly quantusruntime_encodeExtrinsic: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number, number];
|
||||
readonly quantusruntime_extrinsicVersion: (a: number) => number;
|
||||
readonly quantusruntime_new: (a: number, b: number) => [number, number, number];
|
||||
readonly quantusruntime_setSs58Format: (a: number, b: number) => void;
|
||||
readonly quantusruntime_signedExtensions: (a: number) => [number, number, number, number];
|
||||
readonly quantusruntime_signerPayload: (a: number, b: number, c: number, d: number, e: number) => [number, number, number, number];
|
||||
readonly quantusruntime_storageTarget: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number];
|
||||
readonly __wbindgen_externrefs: WebAssembly.Table;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __externref_table_dealloc: (a: number) => void;
|
||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
*
|
||||
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {InitOutput}
|
||||
*/
|
||||
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||
475
packages/quantus-codec/src/generated/quantus_codec.js
Normal file
475
packages/quantus-codec/src/generated/quantus_codec.js
Normal file
@@ -0,0 +1,475 @@
|
||||
/* @ts-self-types="./quantus_codec.d.ts" */
|
||||
|
||||
/**
|
||||
* A loaded runtime description, held across calls so the metadata is parsed
|
||||
* once per spec version rather than once per signature.
|
||||
*/
|
||||
export class QuantusRuntime {
|
||||
__destroy_into_raw() {
|
||||
const ptr = this.__wbg_ptr;
|
||||
this.__wbg_ptr = 0;
|
||||
QuantusRuntimeFinalization.unregister(this);
|
||||
return ptr;
|
||||
}
|
||||
free() {
|
||||
const ptr = this.__destroy_into_raw();
|
||||
wasm.__wbg_quantusruntime_free(ptr, 0);
|
||||
}
|
||||
/**
|
||||
* Decode a bare call — what an approval screen shows the user.
|
||||
* @param {Uint8Array} bytes
|
||||
* @returns {string}
|
||||
*/
|
||||
decodeCall(bytes) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.quantusruntime_decodeCall(this.__wbg_ptr, ptr0, len0);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Decode one extrinsic as this runtime describes it, as JSON.
|
||||
* @param {Uint8Array} blob
|
||||
* @returns {string}
|
||||
*/
|
||||
decodeExtrinsic(blob) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passArray8ToWasm0(blob, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.quantusruntime_decodeExtrinsic(this.__wbg_ptr, ptr0, len0);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Decode a storage value against the type id `storageTarget` reported.
|
||||
* @param {number} value_ty
|
||||
* @param {Uint8Array} bytes
|
||||
* @returns {string}
|
||||
*/
|
||||
decodeStorage(value_ty, bytes) {
|
||||
let deferred3_0;
|
||||
let deferred3_1;
|
||||
try {
|
||||
const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.quantusruntime_decodeStorage(this.__wbg_ptr, value_ty, ptr0, len0);
|
||||
var ptr2 = ret[0];
|
||||
var len2 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr2 = 0; len2 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred3_0 = ptr2;
|
||||
deferred3_1 = len2;
|
||||
return getStringFromWasm0(ptr2, len2);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Encode a call by name. `args` is a JSON object keyed by argument name.
|
||||
* @param {string} pallet
|
||||
* @param {string} call
|
||||
* @param {string} args
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
encodeCall(pallet, call, args) {
|
||||
const ptr0 = passStringToWasm0(pallet, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(call, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ptr2 = passStringToWasm0(args, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len2 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.quantusruntime_encodeCall(this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v4 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v4;
|
||||
}
|
||||
/**
|
||||
* The `extra` alone, which the extrinsic carries and the payload repeats.
|
||||
* @param {string} extensions
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
encodeExtra(extensions) {
|
||||
const ptr0 = passStringToWasm0(extensions, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.quantusruntime_encodeExtra(this.__wbg_ptr, ptr0, len0);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v2;
|
||||
}
|
||||
/**
|
||||
* Assemble a signed extrinsic, ready for `author_submitAndWatchExtrinsic`.
|
||||
* @param {string} address
|
||||
* @param {Uint8Array} signature
|
||||
* @param {Uint8Array} extra
|
||||
* @param {Uint8Array} call
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
encodeExtrinsic(address, signature, extra, call) {
|
||||
const ptr0 = passStringToWasm0(address, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passArray8ToWasm0(signature, wasm.__wbindgen_malloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ptr2 = passArray8ToWasm0(extra, wasm.__wbindgen_malloc);
|
||||
const len2 = WASM_VECTOR_LEN;
|
||||
const ptr3 = passArray8ToWasm0(call, wasm.__wbindgen_malloc);
|
||||
const len3 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.quantusruntime_encodeExtrinsic(this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v5 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v5;
|
||||
}
|
||||
/**
|
||||
* The extrinsic format version this runtime declares.
|
||||
* @returns {number}
|
||||
*/
|
||||
extrinsicVersion() {
|
||||
const ret = wasm.quantusruntime_extrinsicVersion(this.__wbg_ptr);
|
||||
return ret;
|
||||
}
|
||||
/**
|
||||
* Parse metadata as `state_getMetadata` returns it.
|
||||
* @param {Uint8Array} metadata
|
||||
*/
|
||||
constructor(metadata) {
|
||||
const ptr0 = passArray8ToWasm0(metadata, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.quantusruntime_new(ptr0, len0);
|
||||
if (ret[2]) {
|
||||
throw takeFromExternrefTable0(ret[1]);
|
||||
}
|
||||
this.__wbg_ptr = ret[0];
|
||||
QuantusRuntimeFinalization.register(this, this.__wbg_ptr, this);
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Render account ids as SS58 at this prefix when decoding.
|
||||
*
|
||||
* Off until set. The prefix is a property of the chain a caller is talking
|
||||
* to rather than of the metadata, and a guessed one would put a plausible,
|
||||
* wrong address in front of somebody about to approve a transfer.
|
||||
* @param {number} prefix
|
||||
*/
|
||||
setSs58Format(prefix) {
|
||||
wasm.quantusruntime_setSs58Format(this.__wbg_ptr, prefix);
|
||||
}
|
||||
/**
|
||||
* Every signed extension, in order, as
|
||||
* `[{ identifier, needsExtra, needsAdditional }]`.
|
||||
*
|
||||
* The two booleans are what a caller has to satisfy, read from the
|
||||
* registry. A caller that ignores them gets an error rather than a short
|
||||
* payload.
|
||||
* @returns {string}
|
||||
*/
|
||||
signedExtensions() {
|
||||
let deferred2_0;
|
||||
let deferred2_1;
|
||||
try {
|
||||
const ret = wasm.quantusruntime_signedExtensions(this.__wbg_ptr);
|
||||
var ptr1 = ret[0];
|
||||
var len1 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr1 = 0; len1 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred2_0 = ptr1;
|
||||
deferred2_1 = len1;
|
||||
return getStringFromWasm0(ptr1, len1);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* The bytes to sign, given an encoded call and the extension values.
|
||||
*
|
||||
* `extensions` is a JSON object keyed by extension identifier, each value
|
||||
* `{ extra?, additional? }`. Omitting one the runtime declares as non-empty
|
||||
* is an error — see [`Runtime::encode_extensions`].
|
||||
* @param {Uint8Array} call
|
||||
* @param {string} extensions
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
signerPayload(call, extensions) {
|
||||
const ptr0 = passArray8ToWasm0(call, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(extensions, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.quantusruntime_signerPayload(this.__wbg_ptr, ptr0, len0, ptr1, len1);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v3 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v3;
|
||||
}
|
||||
/**
|
||||
* Where a storage value lives, and what it decodes as.
|
||||
*
|
||||
* Returns `{ key, valueTy, default }` — `key` ready for `state_getStorage`,
|
||||
* and `default` the bytes the chain means when it returns nothing, or null
|
||||
* for an entry where nothing means nothing. An unfunded account reads as a
|
||||
* zero balance through the first and as an error through the second, so the
|
||||
* distinction is not a detail.
|
||||
* @param {string} pallet
|
||||
* @param {string} item
|
||||
* @param {string} keys
|
||||
* @returns {string}
|
||||
*/
|
||||
storageTarget(pallet, item, keys) {
|
||||
let deferred5_0;
|
||||
let deferred5_1;
|
||||
try {
|
||||
const ptr0 = passStringToWasm0(pallet, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(item, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ptr2 = passStringToWasm0(keys, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len2 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.quantusruntime_storageTarget(this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2);
|
||||
var ptr4 = ret[0];
|
||||
var len4 = ret[1];
|
||||
if (ret[3]) {
|
||||
ptr4 = 0; len4 = 0;
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
deferred5_0 = ptr4;
|
||||
deferred5_1 = len4;
|
||||
return getStringFromWasm0(ptr4, len4);
|
||||
} finally {
|
||||
wasm.__wbindgen_free(deferred5_0, deferred5_1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Symbol.dispose) QuantusRuntime.prototype[Symbol.dispose] = QuantusRuntime.prototype.free;
|
||||
function __wbg_get_imports() {
|
||||
const import0 = {
|
||||
__proto__: null,
|
||||
__wbg_Error_67e7344beaa85059: function(arg0, arg1) {
|
||||
const ret = Error(getStringFromWasm0(arg0, arg1));
|
||||
return ret;
|
||||
},
|
||||
__wbg___wbindgen_throw_5d9e815e6fdf150f: function(arg0, arg1) {
|
||||
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||
},
|
||||
__wbindgen_init_externref_table: function() {
|
||||
const table = wasm.__wbindgen_externrefs;
|
||||
const offset = table.grow(4);
|
||||
table.set(0, undefined);
|
||||
table.set(offset + 0, undefined);
|
||||
table.set(offset + 1, null);
|
||||
table.set(offset + 2, true);
|
||||
table.set(offset + 3, false);
|
||||
},
|
||||
};
|
||||
return {
|
||||
__proto__: null,
|
||||
"./quantus_codec_bg.js": import0,
|
||||
};
|
||||
}
|
||||
|
||||
const QuantusRuntimeFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||
? { register: () => {}, unregister: () => {} }
|
||||
: new FinalizationRegistry(ptr => wasm.__wbg_quantusruntime_free(ptr, 1));
|
||||
|
||||
function getArrayU8FromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
return decodeText(ptr >>> 0, len);
|
||||
}
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
function getUint8ArrayMemory0() {
|
||||
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
function passArray8ToWasm0(arg, malloc) {
|
||||
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
||||
getUint8ArrayMemory0().set(arg, ptr / 1);
|
||||
WASM_VECTOR_LEN = arg.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function passStringToWasm0(arg, malloc, realloc) {
|
||||
if (realloc === undefined) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
const ptr = malloc(buf.length, 1) >>> 0;
|
||||
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
WASM_VECTOR_LEN = buf.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let len = arg.length;
|
||||
let ptr = malloc(len, 1) >>> 0;
|
||||
|
||||
const mem = getUint8ArrayMemory0();
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (; offset < len; offset++) {
|
||||
const code = arg.charCodeAt(offset);
|
||||
if (code > 0x7F) break;
|
||||
mem[ptr + offset] = code;
|
||||
}
|
||||
if (offset !== len) {
|
||||
if (offset !== 0) {
|
||||
arg = arg.slice(offset);
|
||||
}
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||
const ret = cachedTextEncoder.encodeInto(arg, view);
|
||||
|
||||
offset += ret.written;
|
||||
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||
}
|
||||
|
||||
WASM_VECTOR_LEN = offset;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function takeFromExternrefTable0(idx) {
|
||||
const value = wasm.__wbindgen_externrefs.get(idx);
|
||||
wasm.__externref_table_dealloc(idx);
|
||||
return value;
|
||||
}
|
||||
|
||||
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
cachedTextDecoder.decode();
|
||||
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
||||
let numBytesDecoded = 0;
|
||||
function decodeText(ptr, len) {
|
||||
numBytesDecoded += len;
|
||||
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
||||
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
cachedTextDecoder.decode();
|
||||
numBytesDecoded = len;
|
||||
}
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
const cachedTextEncoder = new TextEncoder();
|
||||
|
||||
if (!('encodeInto' in cachedTextEncoder)) {
|
||||
cachedTextEncoder.encodeInto = function (arg, view) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
view.set(buf);
|
||||
return {
|
||||
read: arg.length,
|
||||
written: buf.length
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
let wasmModule, wasmInstance, wasm;
|
||||
function __wbg_finalize_init(instance, module) {
|
||||
wasmInstance = instance;
|
||||
wasm = instance.exports;
|
||||
wasmModule = module;
|
||||
cachedUint8ArrayMemory0 = null;
|
||||
wasm.__wbindgen_start();
|
||||
return wasm;
|
||||
}
|
||||
|
||||
async function __wbg_load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (!module.ok) {
|
||||
throw new Error(`failed to fetch Wasm: ${module.status} ${module.statusText} fetching '${module.url}'`);
|
||||
}
|
||||
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
try {
|
||||
return await WebAssembly.instantiateStreaming(module, imports);
|
||||
} catch (e) {
|
||||
const validResponse = expectedResponseType(module.type);
|
||||
|
||||
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
|
||||
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||
|
||||
} else { throw e; }
|
||||
}
|
||||
}
|
||||
|
||||
const bytes = await module.arrayBuffer();
|
||||
return await WebAssembly.instantiate(bytes, imports);
|
||||
} else {
|
||||
const instance = await WebAssembly.instantiate(module, imports);
|
||||
|
||||
if (instance instanceof WebAssembly.Instance) {
|
||||
return { instance, module };
|
||||
} else {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
function expectedResponseType(type) {
|
||||
switch (type) {
|
||||
case 'basic': case 'cors': case 'default': return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function initSync(module) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (module !== undefined) {
|
||||
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||
({module} = module)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
const imports = __wbg_get_imports();
|
||||
if (!(module instanceof WebAssembly.Module)) {
|
||||
module = new WebAssembly.Module(module);
|
||||
}
|
||||
const instance = new WebAssembly.Instance(module, imports);
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
export { initSync };
|
||||
6
packages/quantus-codec/src/index.ts
Normal file
6
packages/quantus-codec/src/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright 2026 @quantus/codec authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export { Runtime } from './codec.js';
|
||||
export type { DecodedExtrinsic, ExtensionNeed, ExtensionValue, ExtensionValues, PayloadOptions, StorageTarget } from './codec.js';
|
||||
export { initWasm, isReady } from './init.js';
|
||||
61
packages/quantus-codec/src/init.ts
Normal file
61
packages/quantus-codec/src/init.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2026 @quantus/codec authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { unzlibSync } from 'fflate';
|
||||
|
||||
import { base64Decode } from './base64.js';
|
||||
|
||||
import { bytes, lenOut } from './bytes.js';
|
||||
import { initSync } from './generated/quantus_codec.js';
|
||||
|
||||
/**
|
||||
* Instantiate the WASM, synchronously, from bytes compiled into this file.
|
||||
*
|
||||
* Three constraints shape this, and all three rule out the obvious approach:
|
||||
*
|
||||
* - the background context is an **MV3 service worker**, so there is no DOM, no
|
||||
* reliable `fetch` of extension-relative URLs at arbitrary times, and the
|
||||
* worker can be killed and cold-started between any two messages
|
||||
* - the extension CSP is `script-src 'self' 'wasm-unsafe-eval'`, which permits
|
||||
* compiling WASM but not fetching it from anywhere interesting
|
||||
* - callers are synchronous — `pair.sign()` in the keyring has no `await` to give
|
||||
*
|
||||
* So the WASM is zlib-compressed, base64'd into `bytes.js` at build time, and
|
||||
* instantiated here with wasm-bindgen's `initSync`. Nothing is fetched, and the
|
||||
* whole module is ready before the first call returns.
|
||||
*
|
||||
* Deliberately *not* using `@polkadot/wasm-bridge`: its `Bridge` implements
|
||||
* wasm-bindgen 0.2.79's JS-heap ABI, and this crate is built with 0.2.128, which
|
||||
* uses externref tables. See quantus/wasm#1.
|
||||
*/
|
||||
|
||||
let initialised = false;
|
||||
let initError: string | null = null;
|
||||
|
||||
/**
|
||||
* Ensure the WASM is instantiated. Idempotent and cheap after the first call.
|
||||
*
|
||||
* Returns `null` on success, or the failure reason. It does not throw: a caller
|
||||
* deciding whether to offer a Quantus account at all wants to ask, and an
|
||||
* exception thrown from module scope in a service worker is hard to attribute.
|
||||
*/
|
||||
export function initWasm (): string | null {
|
||||
if (initialised) {
|
||||
return initError;
|
||||
}
|
||||
|
||||
initialised = true;
|
||||
|
||||
try {
|
||||
initSync({ module: unzlibSync(base64Decode(bytes, new Uint8Array(lenOut))) });
|
||||
} catch (error) {
|
||||
initError = (error as Error).message;
|
||||
}
|
||||
|
||||
return initError;
|
||||
}
|
||||
|
||||
/** Whether the WASM is available. Callers that can fall back should ask first. */
|
||||
export function isReady (): boolean {
|
||||
return initWasm() === null;
|
||||
}
|
||||
40
packages/quantus-codec/src/lib.rs
Normal file
40
packages/quantus-codec/src/lib.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright 2026 @quantus/codec authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Metadata-driven SCALE encode and decode for the Quantus chain.
|
||||
//!
|
||||
//! Nothing here 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 crate's author.
|
||||
//!
|
||||
//! That is not fastidiousness. This chain's encoding has changed between
|
||||
//! runtimes — `transactionVersion` has gone 2 → 3 → 6 across four upgrades, and
|
||||
//! each of those is an extrinsic-format change. A signer holding a hand-written
|
||||
//! idea of the format keeps producing signatures after such an upgrade; they are
|
||||
//! cryptographically valid, over the wrong bytes, and the chain reports them as
|
||||
//! `BadProof`, which is what it also reports for a wrong key. See quantus/wasm#3.
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
#[path = "rs/runtime.rs"]
|
||||
pub mod runtime;
|
||||
|
||||
#[path = "rs/decode.rs"]
|
||||
pub mod decode;
|
||||
|
||||
#[path = "rs/encode.rs"]
|
||||
pub mod encode;
|
||||
|
||||
#[path = "rs/storage.rs"]
|
||||
pub mod storage;
|
||||
|
||||
#[path = "rs/ss58.rs"]
|
||||
pub mod ss58;
|
||||
|
||||
#[path = "rs/bindings.rs"]
|
||||
mod bindings;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "rs/tests.rs"]
|
||||
mod tests;
|
||||
251
packages/quantus-codec/src/rs/bindings.rs
Normal file
251
packages/quantus-codec/src/rs/bindings.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
// Copyright 2026 @quantus/codec authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! The `wasm_bindgen` surface.
|
||||
//!
|
||||
//! Deliberately thin: every one of these is a parse of the JS argument, a call
|
||||
//! into a module that knows nothing about JS, and a serialisation back. The
|
||||
//! logic lives in [`crate::runtime`], [`crate::decode`] and [`crate::encode`]
|
||||
//! because `JsError` cannot be constructed outside a wasm target, so anything
|
||||
//! built on it is untestable by `cargo test` — a lesson from quantus/wasm#1,
|
||||
//! where the error paths were the ones that turned out to be wrong.
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::encode::{ExtensionValue, Supplied};
|
||||
use crate::runtime::Runtime;
|
||||
|
||||
/// A loaded runtime description, held across calls so the metadata is parsed
|
||||
/// once per spec version rather than once per signature.
|
||||
#[wasm_bindgen]
|
||||
pub struct QuantusRuntime {
|
||||
inner: Runtime,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl QuantusRuntime {
|
||||
/// Parse metadata as `state_getMetadata` returns it.
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(metadata: &[u8]) -> Result<QuantusRuntime, JsError> {
|
||||
Runtime::from_metadata(metadata)
|
||||
.map(|inner| QuantusRuntime { inner })
|
||||
.map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Render account ids as SS58 at this prefix when decoding.
|
||||
///
|
||||
/// Off until set. The prefix is a property of the chain a caller is talking
|
||||
/// to rather than of the metadata, and a guessed one would put a plausible,
|
||||
/// wrong address in front of somebody about to approve a transfer.
|
||||
#[wasm_bindgen(js_name = setSs58Format)]
|
||||
pub fn set_ss58_format(&mut self, prefix: u16) {
|
||||
self.inner.set_ss58_format(prefix);
|
||||
}
|
||||
|
||||
/// The extrinsic format version this runtime declares.
|
||||
#[wasm_bindgen(js_name = extrinsicVersion)]
|
||||
pub fn extrinsic_version(&self) -> u8 {
|
||||
self.inner.extrinsic_version()
|
||||
}
|
||||
|
||||
/// Every signed extension, in order, as
|
||||
/// `[{ identifier, needsExtra, needsAdditional }]`.
|
||||
///
|
||||
/// The two booleans are what a caller has to satisfy, read from the
|
||||
/// registry. A caller that ignores them gets an error rather than a short
|
||||
/// payload.
|
||||
#[wasm_bindgen(js_name = signedExtensions)]
|
||||
pub fn signed_extensions(&self) -> Result<String, JsError> {
|
||||
let described: Vec<serde_json::Value> = self
|
||||
.inner
|
||||
.extensions()
|
||||
.iter()
|
||||
.map(|e| {
|
||||
serde_json::json!({
|
||||
"identifier": e.identifier,
|
||||
"needsExtra": !self.inner.is_empty_ty_pub(e.ty),
|
||||
"needsAdditional": !self.inner.is_empty_ty_pub(e.additional)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
serde_json::to_string(&described).map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Encode a call by name. `args` is a JSON object keyed by argument name.
|
||||
#[wasm_bindgen(js_name = encodeCall)]
|
||||
pub fn encode_call(&self, pallet: &str, call: &str, args: &str) -> Result<Vec<u8>, JsError> {
|
||||
let args: serde_json::Value =
|
||||
serde_json::from_str(args).map_err(|e| JsError::new(&e.to_string()))?;
|
||||
|
||||
self.inner
|
||||
.encode_call(pallet, call, &args)
|
||||
.map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
|
||||
/// The bytes to sign, given an encoded call and the extension values.
|
||||
///
|
||||
/// `extensions` is a JSON object keyed by extension identifier, each value
|
||||
/// `{ extra?, additional? }`. Omitting one the runtime declares as non-empty
|
||||
/// is an error — see [`Runtime::encode_extensions`].
|
||||
#[wasm_bindgen(js_name = signerPayload)]
|
||||
pub fn signer_payload(&self, call: &[u8], extensions: &str) -> Result<Vec<u8>, JsError> {
|
||||
let encoded = self.encoded_extensions(extensions)?;
|
||||
|
||||
Ok(self.inner.signer_payload(call, &encoded))
|
||||
}
|
||||
|
||||
/// The `extra` alone, which the extrinsic carries and the payload repeats.
|
||||
#[wasm_bindgen(js_name = encodeExtra)]
|
||||
pub fn encode_extra(&self, extensions: &str) -> Result<Vec<u8>, JsError> {
|
||||
Ok(self.encoded_extensions(extensions)?.extra)
|
||||
}
|
||||
|
||||
/// Assemble a signed extrinsic, ready for `author_submitAndWatchExtrinsic`.
|
||||
#[wasm_bindgen(js_name = encodeExtrinsic)]
|
||||
pub fn encode_extrinsic(
|
||||
&self,
|
||||
address: &str,
|
||||
signature: &[u8],
|
||||
extra: &[u8],
|
||||
call: &[u8],
|
||||
) -> Result<Vec<u8>, JsError> {
|
||||
let address: serde_json::Value =
|
||||
serde_json::from_str(address).map_err(|e| JsError::new(&e.to_string()))?;
|
||||
|
||||
self.inner
|
||||
.encode_extrinsic(&address, signature, extra, call)
|
||||
.map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Decode one extrinsic as this runtime describes it, as JSON.
|
||||
#[wasm_bindgen(js_name = decodeExtrinsic)]
|
||||
pub fn decode_extrinsic(&self, blob: &[u8]) -> Result<String, JsError> {
|
||||
let xt = self
|
||||
.inner
|
||||
.decode_extrinsic(blob)
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"version": xt.version,
|
||||
"signed": xt.signed,
|
||||
"address": xt.address,
|
||||
"signature": xt.signature,
|
||||
"extra": xt.extra,
|
||||
"call": xt.call
|
||||
}))
|
||||
.map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Where a storage value lives, and what it decodes as.
|
||||
///
|
||||
/// Returns `{ key, valueTy, default }` — `key` ready for `state_getStorage`,
|
||||
/// and `default` the bytes the chain means when it returns nothing, or null
|
||||
/// for an entry where nothing means nothing. An unfunded account reads as a
|
||||
/// zero balance through the first and as an error through the second, so the
|
||||
/// distinction is not a detail.
|
||||
#[wasm_bindgen(js_name = storageTarget)]
|
||||
pub fn storage_target(&self, pallet: &str, item: &str, keys: &str) -> Result<String, JsError> {
|
||||
let keys: Vec<serde_json::Value> =
|
||||
serde_json::from_str(keys).map_err(|e| JsError::new(&e.to_string()))?;
|
||||
|
||||
let target = self
|
||||
.inner
|
||||
.storage_target(pallet, item, &keys)
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"default": target.default.as_deref().map(crate::storage::hex),
|
||||
"key": crate::storage::hex(&target.key),
|
||||
"valueTy": target.value_ty
|
||||
}))
|
||||
.map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Decode a storage value against the type id `storageTarget` reported.
|
||||
#[wasm_bindgen(js_name = decodeStorage)]
|
||||
pub fn decode_storage(&self, value_ty: u32, bytes: &[u8]) -> Result<String, JsError> {
|
||||
let value = self
|
||||
.inner
|
||||
.decode_storage_value(value_ty, bytes)
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
|
||||
serde_json::to_string(&value).map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
|
||||
/// Decode a bare call — what an approval screen shows the user.
|
||||
#[wasm_bindgen(js_name = decodeCall)]
|
||||
pub fn decode_call(&self, bytes: &[u8]) -> Result<String, JsError> {
|
||||
let call = self
|
||||
.inner
|
||||
.decode_call(bytes)
|
||||
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||
|
||||
serde_json::to_string(&call).map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
|
||||
fn encoded_extensions(
|
||||
&self,
|
||||
extensions: &str,
|
||||
) -> Result<crate::encode::EncodedExtensions, JsError> {
|
||||
let parsed: BTreeMap<String, serde_json::Value> =
|
||||
serde_json::from_str(extensions).map_err(|e| JsError::new(&e.to_string()))?;
|
||||
|
||||
let mut values: BTreeMap<String, ExtensionValue> = BTreeMap::new();
|
||||
|
||||
for (identifier, v) in parsed {
|
||||
values.insert(
|
||||
identifier,
|
||||
ExtensionValue {
|
||||
extra: half(&v, "extra")?,
|
||||
additional: half(&v, "additional")?,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
self.inner
|
||||
.encode_extensions(&values)
|
||||
.map_err(|e| JsError::new(&e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// One half of an extension value, as JSON or as pre-encoded bytes.
|
||||
///
|
||||
/// `extra` / `additional` are interpreted against the runtime's declared type;
|
||||
/// `extraHex` / `additionalHex` are bytes the caller encoded itself, which are
|
||||
/// validated by round trip rather than taken on trust. Supplying both is a
|
||||
/// contradiction, not a preference, so it is refused.
|
||||
fn half(value: &serde_json::Value, name: &str) -> Result<Option<Supplied>, JsError> {
|
||||
let json = value.get(name);
|
||||
let hex = value.get(alloc::format!("{name}Hex"));
|
||||
|
||||
match (json, hex) {
|
||||
(Some(_), Some(_)) => Err(JsError::new(&alloc::format!(
|
||||
"{name} and {name}Hex are both set"
|
||||
))),
|
||||
(Some(v), None) => Ok(Some(Supplied::Json(v.clone()))),
|
||||
(None, Some(v)) => {
|
||||
let s = v
|
||||
.as_str()
|
||||
.ok_or_else(|| JsError::new(&alloc::format!("{name}Hex is not a string")))?;
|
||||
let s = s.strip_prefix("0x").unwrap_or(s);
|
||||
|
||||
if s.len() % 2 != 0 {
|
||||
return Err(JsError::new(&alloc::format!("{name}Hex is not whole bytes")));
|
||||
}
|
||||
|
||||
let bytes: Result<Vec<u8>, _> = (0..s.len() / 2)
|
||||
.map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16))
|
||||
.collect();
|
||||
|
||||
Ok(Some(Supplied::Raw(bytes.map_err(|e| {
|
||||
JsError::new(&alloc::format!("{name}Hex: {e}"))
|
||||
})?)))
|
||||
}
|
||||
(None, None) => Ok(None),
|
||||
}
|
||||
}
|
||||
321
packages/quantus-codec/src/rs/decode.rs
Normal file
321
packages/quantus-codec/src/rs/decode.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
// Copyright 2026 @quantus/codec authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Reading a chain's own data using the chain's own description of it.
|
||||
|
||||
use alloc::format;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use parity_scale_codec::Decode;
|
||||
|
||||
use crate::runtime::{CodecError, Runtime};
|
||||
|
||||
/// One decoded extrinsic, named as the runtime names it.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct DecodedExtrinsic {
|
||||
/// The **version** from the preamble byte — its low six bits, not the byte.
|
||||
pub version: u8,
|
||||
/// Whether the preamble's type tag says signed.
|
||||
pub signed: bool,
|
||||
/// The address as the runtime's `Address` type decodes, rendered as JSON.
|
||||
pub address: serde_json::Value,
|
||||
/// The signature, rendered as JSON. For Quantus this is a
|
||||
/// `DilithiumSignatureScheme` variant carrying `sig ‖ pk`.
|
||||
pub signature: serde_json::Value,
|
||||
/// The signed extensions as submitted — mortality, nonce, tip, and whatever
|
||||
/// else this runtime declares.
|
||||
pub extra: serde_json::Value,
|
||||
/// The `extra` exactly as it appeared on the wire. Kept because it is half
|
||||
/// of the signed payload, and re-encoding it from `extra` would be a second
|
||||
/// implementation that could disagree.
|
||||
pub extra_bytes: Vec<u8>,
|
||||
/// The call, SCALE-encoded, as it appeared on the wire.
|
||||
pub call_bytes: Vec<u8>,
|
||||
/// The call, decoded.
|
||||
pub call: serde_json::Value,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
/// Decode one extrinsic, exactly as this runtime describes it.
|
||||
///
|
||||
/// ## The preamble byte is not the version
|
||||
///
|
||||
/// The **top two bits are a type tag** and the low six are the version:
|
||||
/// `0b00` bare, `0b10` signed, `0b01` general. Quantus emits `0x84` —
|
||||
/// signed, v4 — and `0x05` — bare, v**5** — in the same block, while the
|
||||
/// metadata declares extrinsic version 4. Three different numbers, all
|
||||
/// correct.
|
||||
///
|
||||
/// A decoder that reads the byte as a version and checks it against the
|
||||
/// metadata rejects every timestamp inherent on the chain. `@polkadot/api`
|
||||
/// does exactly that, which is why it cannot read a single Quantus block
|
||||
/// (quantus/api#1), and it is the first thing to break when someone
|
||||
/// "simplifies" this function.
|
||||
///
|
||||
/// `blob` is the extrinsic as the node hands it over: its own length prefix
|
||||
/// first. A partial read is refused — trailing bytes mean the metadata does
|
||||
/// not match these bytes, and a decoder that shrugs at that is how a block
|
||||
/// gets silently mis-read after an upgrade.
|
||||
pub fn decode_extrinsic(&self, blob: &[u8]) -> Result<DecodedExtrinsic, CodecError> {
|
||||
let mut cursor = blob;
|
||||
let declared = <parity_scale_codec::Compact<u64>>::decode(&mut cursor)
|
||||
.map_err(|e| CodecError::Decode(format!("no length prefix: {e}")))?
|
||||
.0 as usize;
|
||||
|
||||
if cursor.len() != declared {
|
||||
return Err(CodecError::Decode(format!(
|
||||
"declared {declared} bytes, {} present",
|
||||
cursor.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let preamble = *cursor
|
||||
.first()
|
||||
.ok_or_else(|| CodecError::Decode("empty extrinsic".to_string()))?;
|
||||
cursor = &cursor[1..];
|
||||
|
||||
let signed = preamble & 0b1100_0000 == 0b1000_0000;
|
||||
let version = preamble & 0b0011_1111;
|
||||
let tys = self.extrinsic;
|
||||
|
||||
let (address, signature, extra, extra_bytes) = if signed {
|
||||
let address = self.decode_at(tys.address, &mut cursor, "address")?;
|
||||
let signature = self.decode_at(tys.signature, &mut cursor, "signature")?;
|
||||
|
||||
let before = cursor;
|
||||
let extra = self.decode_at(tys.extra, &mut cursor, "signed extensions")?;
|
||||
let extra_bytes = before[..before.len() - cursor.len()].to_vec();
|
||||
|
||||
(address, signature, extra, extra_bytes)
|
||||
} else {
|
||||
(
|
||||
serde_json::Value::Null,
|
||||
serde_json::Value::Null,
|
||||
serde_json::Value::Null,
|
||||
Vec::new(),
|
||||
)
|
||||
};
|
||||
|
||||
let call_bytes = cursor.to_vec();
|
||||
let call = self.decode_at(tys.call, &mut cursor, "call")?;
|
||||
|
||||
if !cursor.is_empty() {
|
||||
return Err(CodecError::Decode(format!(
|
||||
"{} trailing bytes; metadata does not match this extrinsic",
|
||||
cursor.len()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(DecodedExtrinsic {
|
||||
version,
|
||||
signed,
|
||||
address,
|
||||
signature,
|
||||
extra,
|
||||
extra_bytes,
|
||||
call_bytes: call_bytes[..call_bytes.len() - cursor.len()].to_vec(),
|
||||
call,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decode a bare call — what an approval screen needs to say what is about
|
||||
/// to be authorised.
|
||||
pub fn decode_call(&self, bytes: &[u8]) -> Result<serde_json::Value, CodecError> {
|
||||
let mut cursor = bytes;
|
||||
let call = self.decode_at(self.extrinsic.call, &mut cursor, "call")?;
|
||||
|
||||
if !cursor.is_empty() {
|
||||
return Err(CodecError::Decode(format!(
|
||||
"{} trailing bytes after call",
|
||||
cursor.len()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(call)
|
||||
}
|
||||
|
||||
fn decode_at(
|
||||
&self,
|
||||
ty: u32,
|
||||
cursor: &mut &[u8],
|
||||
what: &str,
|
||||
) -> Result<serde_json::Value, CodecError> {
|
||||
let value = self
|
||||
.decode_checked(ty, cursor)
|
||||
.map_err(|e| CodecError::Decode(format!("{what}: {e}")))?;
|
||||
|
||||
Ok(self.render(&value))
|
||||
}
|
||||
|
||||
/// Decode one registry type, walking the bytes first without building
|
||||
/// anything from them.
|
||||
///
|
||||
/// **`scale_value` sizes a sequence's `Vec` from the length prefix before it
|
||||
/// decodes a single item.** A blob that disagrees with the registry can
|
||||
/// therefore ask for an allocation of any size at all, and Rust aborts on a
|
||||
/// failed one — so there is no `Err` for a caller to catch, and `.ok()` at
|
||||
/// the call site cannot help. blackbeard.observer found this in production
|
||||
/// as a 76 GiB request that took the daemon down every two minutes.
|
||||
///
|
||||
/// `scale_decode`'s `IgnoreVisitor` walks the same bytes against the same
|
||||
/// type and allocates nothing at all, so a length that cannot be satisfied
|
||||
/// runs out of input on the first item and comes back as an error. The
|
||||
/// second pass costs one more walk of a few kilobytes.
|
||||
pub(crate) fn decode_checked(
|
||||
&self,
|
||||
ty: u32,
|
||||
cursor: &mut &[u8],
|
||||
) -> Result<scale_value::Value<u32>, String> {
|
||||
let mut probe: &[u8] = cursor;
|
||||
|
||||
scale_decode::visitor::decode_with_visitor(
|
||||
&mut probe,
|
||||
ty,
|
||||
self.types(),
|
||||
scale_decode::visitor::IgnoreVisitor::<scale_info::PortableRegistry>::new(),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
scale_value::scale::decode_as_type(cursor, ty, self.types()).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
/// Render a decoded value as JSON, for the boundary to JavaScript.
|
||||
///
|
||||
/// Byte sequences become `0x…` hex rather than arrays of numbers: an account
|
||||
/// id as 32 JSON integers is technically the same information and useless to
|
||||
/// every consumer, and a 7219-byte signature as an array is 30 KiB of JSON.
|
||||
///
|
||||
/// Account ids become SS58 when a prefix has been set. `scale_value` carries
|
||||
/// each value's registry type id as its context, so the check is on the type
|
||||
/// the runtime declared and not on the shape of the bytes — a block hash is
|
||||
/// also 32 bytes, and rendering one as an address would be a lie.
|
||||
pub(crate) fn render(&self, value: &scale_value::Value<u32>) -> serde_json::Value {
|
||||
use scale_value::{Composite, Primitive, ValueDef};
|
||||
|
||||
if let Some(prefix) = self.ss58_format {
|
||||
if self.account_tys.contains(&value.context) {
|
||||
if let Some(bytes) = account_bytes(value) {
|
||||
return serde_json::Value::String(crate::ss58::encode(prefix, &bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match &value.value {
|
||||
ValueDef::Primitive(p) => match p {
|
||||
Primitive::Bool(b) => serde_json::Value::Bool(*b),
|
||||
Primitive::Char(c) => serde_json::Value::String(c.to_string()),
|
||||
Primitive::String(s) => serde_json::Value::String(s.clone()),
|
||||
// **Every** integer renders as a decimal string, whatever its width.
|
||||
//
|
||||
// A u128 balance does not survive a JSON number — this chain has 12
|
||||
// decimal places, so ordinary amounts pass 2^53 — and `scale_value`
|
||||
// widens every unsigned integer to u128 anyway, so the width is not
|
||||
// available here to switch on. Emitting a number when it happens to
|
||||
// fit and a string when it does not would make a consumer handle both
|
||||
// shapes for the same field depending on the value, which is worse
|
||||
// than either. Strings, always, and the caller parses what it knows.
|
||||
Primitive::U128(n) => serde_json::Value::String(n.to_string()),
|
||||
Primitive::I128(n) => serde_json::Value::String(n.to_string()),
|
||||
Primitive::U256(b) | Primitive::I256(b) => serde_json::Value::String(hex(b)),
|
||||
},
|
||||
ValueDef::Composite(Composite::Named(fields)) => serde_json::Value::Object(
|
||||
fields
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), self.render(v)))
|
||||
.collect(),
|
||||
),
|
||||
ValueDef::Composite(Composite::Unnamed(values)) => {
|
||||
if let Some(bytes) = as_bytes(values) {
|
||||
serde_json::Value::String(hex(&bytes))
|
||||
} else if values.len() == 1 {
|
||||
// A newtype wrapper is noise; unwrap it so `Compact<u64>` reads
|
||||
// as a number rather than a one-element array.
|
||||
self.render(&values[0])
|
||||
} else {
|
||||
serde_json::Value::Array(values.iter().map(|v| self.render(v)).collect())
|
||||
}
|
||||
}
|
||||
ValueDef::Variant(v) => {
|
||||
let inner = self.render(&scale_value::Value {
|
||||
value: ValueDef::Composite(v.values.clone()),
|
||||
context: value.context,
|
||||
});
|
||||
|
||||
// `Era::Immortal` and friends carry nothing; render them as the name
|
||||
// alone rather than `{"Immortal": []}`.
|
||||
match &inner {
|
||||
serde_json::Value::Array(a) if a.is_empty() => {
|
||||
serde_json::Value::String(v.name.clone())
|
||||
}
|
||||
serde_json::Value::Object(o) if o.is_empty() => {
|
||||
serde_json::Value::String(v.name.clone())
|
||||
}
|
||||
_ => {
|
||||
let mut map = serde_json::Map::new();
|
||||
map.insert(v.name.clone(), inner);
|
||||
serde_json::Value::Object(map)
|
||||
}
|
||||
}
|
||||
}
|
||||
ValueDef::BitSequence(bits) => {
|
||||
serde_json::Value::Array(bits.iter().map(serde_json::Value::Bool).collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A sequence of `u8` primitives, if that is what this is.
|
||||
fn as_bytes(values: &[scale_value::Value<u32>]) -> Option<Vec<u8>> {
|
||||
use scale_value::{Primitive, ValueDef};
|
||||
|
||||
if values.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
values
|
||||
.iter()
|
||||
.map(|v| match &v.value {
|
||||
ValueDef::Primitive(Primitive::U128(n)) if *n < 256 => Some(*n as u8),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
let mut s = String::with_capacity(2 + bytes.len() * 2);
|
||||
s.push_str("0x");
|
||||
|
||||
for b in bytes {
|
||||
s.push(char::from_digit((b >> 4) as u32, 16).unwrap());
|
||||
s.push(char::from_digit((b & 0x0f) as u32, 16).unwrap());
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
/// The 32 bytes behind an account id, whatever wrapper the registry put round it.
|
||||
///
|
||||
/// `AccountId32` is a newtype over `[u8; 32]`, so the decoded value is a
|
||||
/// composite of a composite of primitives; a registry could also describe it
|
||||
/// flat. Anything that is not exactly 32 bytes is not an account id and is left
|
||||
/// to render as itself.
|
||||
fn account_bytes(value: &scale_value::Value<u32>) -> Option<Vec<u8>> {
|
||||
use scale_value::{Composite, ValueDef};
|
||||
|
||||
match &value.value {
|
||||
ValueDef::Composite(Composite::Unnamed(values)) => {
|
||||
if values.len() == 1 {
|
||||
return account_bytes(&values[0]);
|
||||
}
|
||||
|
||||
as_bytes(values).filter(|b| b.len() == 32)
|
||||
}
|
||||
ValueDef::Composite(Composite::Named(fields)) if fields.len() == 1 => {
|
||||
account_bytes(&fields[0].1)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
578
packages/quantus-codec/src/rs/encode.rs
Normal file
578
packages/quantus-codec/src/rs/encode.rs
Normal file
@@ -0,0 +1,578 @@
|
||||
// Copyright 2026 @quantus/codec authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Building the bytes that get signed, and the extrinsic that carries them.
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::format;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use parity_scale_codec::Encode;
|
||||
use scale_value::{Composite, Primitive, Value, ValueDef};
|
||||
|
||||
use crate::runtime::{CodecError, Runtime};
|
||||
|
||||
/// A value for one half of one signed extension.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Supplied {
|
||||
/// Interpreted against the type the runtime declares — see
|
||||
/// [`Runtime::json_to_value`].
|
||||
Json(serde_json::Value),
|
||||
/// Already SCALE-encoded by whoever is asking for the signature.
|
||||
///
|
||||
/// A dapp hands the wallet an `era` as opaque bytes, having encoded it
|
||||
/// itself, and there is no way to render those as JSON without knowing the
|
||||
/// era algorithm — which is exactly the kind of knowledge this crate refuses
|
||||
/// to hold. So they are accepted, but **not on trust**: [`Runtime::encode_half`]
|
||||
/// decodes them against the declared type and re-encodes them, and anything
|
||||
/// that does not round-trip is rejected rather than signed.
|
||||
Raw(Vec<u8>),
|
||||
}
|
||||
|
||||
/// What the caller knows about one signed extension.
|
||||
///
|
||||
/// Both halves are optional because most extensions need neither: a zero-sized
|
||||
/// `ty` contributes nothing to the extrinsic, and a zero-sized `additional`
|
||||
/// contributes nothing to the payload. Supplying a value for a zero-sized type
|
||||
/// is not an error; *omitting* one for a non-zero-sized type is.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ExtensionValue {
|
||||
pub extra: Option<Supplied>,
|
||||
pub additional: Option<Supplied>,
|
||||
}
|
||||
|
||||
/// The two byte strings a signed extrinsic needs from its extensions.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EncodedExtensions {
|
||||
/// Goes into the extrinsic, after the signature.
|
||||
pub extra: Vec<u8>,
|
||||
/// Goes into the signed payload only — never on the wire.
|
||||
pub additional: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
/// Encode a call by name, against this runtime's own call type.
|
||||
pub fn encode_call(
|
||||
&self,
|
||||
pallet: &str,
|
||||
call: &str,
|
||||
args: &serde_json::Value,
|
||||
) -> Result<Vec<u8>, CodecError> {
|
||||
// The outer `Call` enum is a variant per pallet, and each of those
|
||||
// carries that pallet's own call enum. So a call is two nested variants
|
||||
// by name, and the indices — which are what actually go on the wire —
|
||||
// come from the registry rather than from a table in this crate.
|
||||
let pallet_ty = self.extrinsic.call;
|
||||
let inner = Value::variant(
|
||||
call.to_string(),
|
||||
self.composite_for_call(pallet, call, args)?,
|
||||
);
|
||||
let outer = Value::variant(
|
||||
pallet.to_string(),
|
||||
Composite::Unnamed(alloc::vec![inner]),
|
||||
);
|
||||
|
||||
let mut out = Vec::new();
|
||||
|
||||
scale_value::scale::encode_as_type(&outer, pallet_ty, self.types(), &mut out)
|
||||
.map_err(|e| CodecError::Encode(format!("{pallet}.{call}: {e}")))?;
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Build the argument composite for one call, interpreting `args` against
|
||||
/// the types the runtime declares for it.
|
||||
fn composite_for_call(
|
||||
&self,
|
||||
pallet: &str,
|
||||
call: &str,
|
||||
args: &serde_json::Value,
|
||||
) -> Result<Composite<()>, CodecError> {
|
||||
let calls_ty = self
|
||||
.calls
|
||||
.get(pallet)
|
||||
.copied()
|
||||
.ok_or_else(|| CodecError::NoSuchCall(pallet.to_string()))?;
|
||||
|
||||
let variant = match self.types().resolve(calls_ty).map(|t| &t.type_def) {
|
||||
Some(scale_info::TypeDef::Variant(v)) => v
|
||||
.variants
|
||||
.iter()
|
||||
.find(|v| v.name == call)
|
||||
.ok_or_else(|| CodecError::NoSuchCall(format!("{pallet}.{call}")))?,
|
||||
_ => return Err(CodecError::NoSuchCall(format!("{pallet}.{call}"))),
|
||||
};
|
||||
|
||||
let mut fields = Vec::new();
|
||||
|
||||
for field in &variant.fields {
|
||||
let name = field.name.clone().unwrap_or_default();
|
||||
let supplied = args.get(&name).ok_or_else(|| {
|
||||
CodecError::Encode(format!("{pallet}.{call}: no value for argument {name}"))
|
||||
})?;
|
||||
|
||||
fields.push((name, self.json_to_value(supplied, field.ty.id)?));
|
||||
}
|
||||
|
||||
Ok(Composite::Named(fields))
|
||||
}
|
||||
|
||||
/// Encode every signed extension this runtime declares, in order.
|
||||
///
|
||||
/// ## Why an unsupplied extension is fatal
|
||||
///
|
||||
/// `@polkadot/api` logs `Unknown signed extensions … treating them as
|
||||
/// no-effect` and writes zero bytes for anything it does not recognise. That
|
||||
/// guess is correct only while every unrecognised extension happens to be
|
||||
/// zero-sized, and when it stops being correct the wallet keeps signing —
|
||||
/// valid signatures over a payload that is missing bytes the runtime put
|
||||
/// there. The chain calls that `BadProof`, which is also what it calls a
|
||||
/// wrong key, so the failure is 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 and needs no value; anything else must be
|
||||
/// supplied by the caller or this refuses to build a payload at all. A
|
||||
/// wallet that cannot sign is a bug report; a wallet that signs the wrong
|
||||
/// bytes is a support case that never gets diagnosed.
|
||||
pub fn encode_extensions(
|
||||
&self,
|
||||
values: &BTreeMap<String, ExtensionValue>,
|
||||
) -> Result<EncodedExtensions, CodecError> {
|
||||
let mut extra = Vec::new();
|
||||
let mut additional = Vec::new();
|
||||
|
||||
for def in &self.extensions {
|
||||
let supplied = values.get(&def.identifier);
|
||||
|
||||
self.encode_half(
|
||||
def.ty,
|
||||
supplied.and_then(|v| v.extra.as_ref()),
|
||||
&def.identifier,
|
||||
&mut extra,
|
||||
)?;
|
||||
|
||||
self.encode_half(
|
||||
def.additional,
|
||||
supplied.and_then(|v| v.additional.as_ref()),
|
||||
&def.identifier,
|
||||
&mut additional,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(EncodedExtensions { extra, additional })
|
||||
}
|
||||
|
||||
fn encode_half(
|
||||
&self,
|
||||
ty: u32,
|
||||
supplied: Option<&Supplied>,
|
||||
identifier: &str,
|
||||
out: &mut Vec<u8>,
|
||||
) -> Result<(), CodecError> {
|
||||
if self.is_empty_ty(ty) {
|
||||
// Encodes to nothing whether or not a value was supplied. Writing
|
||||
// nothing here is a reading of the registry, not an assumption.
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let converted = match supplied.ok_or_else(|| {
|
||||
CodecError::MissingExtension(identifier.to_string())
|
||||
})? {
|
||||
Supplied::Json(value) => self.json_to_value(value, ty)?,
|
||||
// Round-tripped rather than appended. Bytes that decode against the
|
||||
// declared type and re-encode to themselves are the *only* bytes the
|
||||
// runtime could have meant; anything else — a short read, trailing
|
||||
// bytes, a non-canonical compact — is a disagreement about the format
|
||||
// that would otherwise be signed and only surface as `BadProof`.
|
||||
Supplied::Raw(bytes) => {
|
||||
let mut cursor = &bytes[..];
|
||||
let value = self
|
||||
.decode_checked(ty, &mut cursor)
|
||||
.map_err(|e| CodecError::Encode(format!("{identifier}: {e}")))?;
|
||||
|
||||
if !cursor.is_empty() {
|
||||
return Err(CodecError::Encode(format!(
|
||||
"{identifier}: {} trailing bytes",
|
||||
cursor.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut check = Vec::new();
|
||||
|
||||
scale_value::scale::encode_as_type(&value, ty, self.types(), &mut check)
|
||||
.map_err(|e| CodecError::Encode(format!("{identifier}: {e}")))?;
|
||||
|
||||
if check != *bytes {
|
||||
return Err(CodecError::Encode(format!(
|
||||
"{identifier}: supplied bytes do not round-trip against the runtime's type"
|
||||
)));
|
||||
}
|
||||
|
||||
out.extend_from_slice(bytes);
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
scale_value::scale::encode_as_type(&converted, ty, self.types(), out)
|
||||
.map_err(|e| CodecError::Encode(format!("{identifier}: {e}")))
|
||||
}
|
||||
|
||||
/// The bytes a signer signs: `call ‖ extra ‖ additional`.
|
||||
///
|
||||
/// Substrate's own rule from `unchecked_extrinsic.rs` — a payload longer than
|
||||
/// 256 bytes is signed as its BLAKE2b-256 hash — is **not** applied here.
|
||||
/// That is the caller's, because the hash belongs with the signing code that
|
||||
/// also chooses the FIPS 204 context, and splitting one rule across two
|
||||
/// packages is how the halves drift apart.
|
||||
pub fn signer_payload(
|
||||
&self,
|
||||
call: &[u8],
|
||||
extensions: &EncodedExtensions,
|
||||
) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(call.len() + extensions.extra.len() + extensions.additional.len());
|
||||
|
||||
out.extend_from_slice(call);
|
||||
out.extend_from_slice(&extensions.extra);
|
||||
out.extend_from_slice(&extensions.additional);
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Assemble a signed extrinsic.
|
||||
///
|
||||
/// The preamble is `0b10 << 6 | version`: the type tag says signed and the
|
||||
/// low six bits carry the version the metadata declares. Not a hard-coded
|
||||
/// `0x84` — if this runtime ever declares a different extrinsic version, the
|
||||
/// byte follows it.
|
||||
///
|
||||
/// `signature` is the already-encoded `Signature` type, variant byte
|
||||
/// included: the signing side knows which ML-DSA scheme the key is, and
|
||||
/// re-deriving it here from the byte length would be a second source of
|
||||
/// truth. It is written raw — a fixed-size array takes **no compact length
|
||||
/// prefix**, which is the detail that a `Vec<u8>`-shaped assumption gets
|
||||
/// wrong by exactly two bytes.
|
||||
pub fn encode_extrinsic(
|
||||
&self,
|
||||
address: &serde_json::Value,
|
||||
signature: &[u8],
|
||||
extra: &[u8],
|
||||
call: &[u8],
|
||||
) -> Result<Vec<u8>, CodecError> {
|
||||
let mut body = Vec::new();
|
||||
|
||||
body.push(0b1000_0000 | (self.extrinsic_version() & 0b0011_1111));
|
||||
|
||||
let addr = self.json_to_value(address, self.extrinsic.address)?;
|
||||
|
||||
scale_value::scale::encode_as_type(&addr, self.extrinsic.address, self.types(), &mut body)
|
||||
.map_err(|e| CodecError::Encode(format!("address: {e}")))?;
|
||||
|
||||
body.extend_from_slice(signature);
|
||||
body.extend_from_slice(extra);
|
||||
body.extend_from_slice(call);
|
||||
|
||||
// The node expects the extrinsic length-prefixed.
|
||||
let mut out = parity_scale_codec::Compact(body.len() as u64).encode();
|
||||
|
||||
out.extend_from_slice(&body);
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Interpret a JSON value as a particular registry type.
|
||||
///
|
||||
/// Type-directed on purpose. The same JSON string `"0xa5aa…"` is an
|
||||
/// `AccountId32`, an `H256` or a `Vec<u8>` depending only on what the runtime
|
||||
/// says goes there, and JSON carries no way to tell them apart. Asking the
|
||||
/// registry is the only way that stays right across an upgrade.
|
||||
pub(crate) fn json_to_value(
|
||||
&self,
|
||||
json: &serde_json::Value,
|
||||
ty: u32,
|
||||
) -> Result<Value<()>, CodecError> {
|
||||
use scale_info::TypeDef;
|
||||
|
||||
let def = self
|
||||
.types()
|
||||
.resolve(ty)
|
||||
.map(|t| &t.type_def)
|
||||
.ok_or_else(|| CodecError::Encode(format!("no registry type {ty}")))?;
|
||||
|
||||
match def {
|
||||
TypeDef::Compact(c) => self.json_to_value(json, c.type_param.id),
|
||||
|
||||
TypeDef::Primitive(p) => primitive(json, p),
|
||||
|
||||
TypeDef::Array(a) => {
|
||||
let inner = a.type_param.id;
|
||||
let want = a.len as usize;
|
||||
|
||||
if let Some(bytes) = hex_bytes(json) {
|
||||
if bytes.len() != want {
|
||||
return Err(CodecError::Encode(format!(
|
||||
"expected {want} bytes, got {}",
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
return Ok(byte_composite(&bytes));
|
||||
}
|
||||
|
||||
self.unnamed(json, |_| inner)
|
||||
}
|
||||
|
||||
TypeDef::Sequence(s) => {
|
||||
if let Some(bytes) = hex_bytes(json) {
|
||||
return Ok(byte_composite(&bytes));
|
||||
}
|
||||
|
||||
self.unnamed(json, |_| s.type_param.id)
|
||||
}
|
||||
|
||||
TypeDef::Tuple(t) => {
|
||||
// An empty tuple is the unit type; JSON `null` and an empty
|
||||
// array both mean it, and so does anything else, since it
|
||||
// encodes to no bytes either way.
|
||||
if t.fields.is_empty() {
|
||||
return Ok(Value::unnamed_composite([]));
|
||||
}
|
||||
|
||||
let ids: Vec<u32> = t.fields.iter().map(|f| f.id).collect();
|
||||
|
||||
self.unnamed(json, move |i| ids[i.min(ids.len() - 1)])
|
||||
}
|
||||
|
||||
TypeDef::Composite(c) => {
|
||||
// A single-field struct is transparent, named or not, unless the
|
||||
// caller actually spelled the field out. `AccountId32(pub [u8;
|
||||
// 32])` should take the hex string its inner array takes, and
|
||||
// `CheckMetadataHash { mode }` should take `"Disabled"` — neither
|
||||
// wrapper is something a caller should have to know about, and
|
||||
// both are wrappers the *runtime* chose, so the registry is what
|
||||
// tells us they are there.
|
||||
if c.fields.len() == 1 {
|
||||
let name = c.fields[0].name.clone();
|
||||
let spelled_out = name
|
||||
.as_ref()
|
||||
.zip(json.as_object())
|
||||
.is_some_and(|(n, map)| map.contains_key(n.as_str()));
|
||||
|
||||
if !spelled_out {
|
||||
let inner = self.json_to_value(json, c.fields[0].ty.id)?;
|
||||
|
||||
return Ok(match name {
|
||||
Some(n) => Value {
|
||||
value: ValueDef::Composite(Composite::Named(alloc::vec![(n, inner)])),
|
||||
context: (),
|
||||
},
|
||||
None => Value::unnamed_composite([inner]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
match json {
|
||||
serde_json::Value::Object(map) => {
|
||||
let mut fields = Vec::new();
|
||||
|
||||
for f in &c.fields {
|
||||
let name = f.name.clone().unwrap_or_default();
|
||||
let v = map.get(&name).ok_or_else(|| {
|
||||
CodecError::Encode(format!("no value for field {name}"))
|
||||
})?;
|
||||
|
||||
fields.push((name, self.json_to_value(v, f.ty.id)?));
|
||||
}
|
||||
|
||||
Ok(Value {
|
||||
value: ValueDef::Composite(Composite::Named(fields)),
|
||||
context: (),
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
let ids: Vec<u32> = c.fields.iter().map(|f| f.ty.id).collect();
|
||||
|
||||
self.unnamed(json, move |i| ids[i.min(ids.len().saturating_sub(1))])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TypeDef::Variant(v) => {
|
||||
// Two spellings, both unambiguous: `"Immortal"` for a variant
|
||||
// that carries nothing, `{"Id": "0x…"}` for one that does.
|
||||
let (name, payload) = match json {
|
||||
serde_json::Value::String(s) => (s.clone(), None),
|
||||
serde_json::Value::Null => ("None".to_string(), None),
|
||||
serde_json::Value::Object(map) if map.len() == 1 => {
|
||||
let (k, v) = map.iter().next().expect("len == 1");
|
||||
|
||||
(k.clone(), Some(v))
|
||||
}
|
||||
_ => {
|
||||
return Err(CodecError::Encode(format!(
|
||||
"cannot read {json} as a variant"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let variant = v
|
||||
.variants
|
||||
.iter()
|
||||
.find(|x| x.name == name)
|
||||
.ok_or_else(|| CodecError::Encode(format!("no variant {name}")))?;
|
||||
|
||||
let named = variant.fields.iter().all(|f| f.name.is_some());
|
||||
|
||||
let composite = match (payload, variant.fields.len()) {
|
||||
(_, 0) => Composite::Unnamed(Vec::new()),
|
||||
// A call's arguments arrive as an object keyed by the names
|
||||
// the runtime gives them, at any depth — a `Utility.batch_all`
|
||||
// carries whole calls in a `Vec<RuntimeCall>`, and each of
|
||||
// those is this same shape again. Before this, only the
|
||||
// outermost call could be spelled that way and anything
|
||||
// nested had to be a positional array.
|
||||
(Some(serde_json::Value::Object(map)), _) if named => {
|
||||
let mut fields = Vec::new();
|
||||
|
||||
for f in &variant.fields {
|
||||
let field = f.name.clone().unwrap_or_default();
|
||||
let v = map.get(&field).ok_or_else(|| {
|
||||
CodecError::Encode(format!("{name}: no value for {field}"))
|
||||
})?;
|
||||
|
||||
fields.push((field, self.json_to_value(v, f.ty.id)?));
|
||||
}
|
||||
|
||||
Composite::Named(fields)
|
||||
}
|
||||
(Some(p), 1) => {
|
||||
Composite::Unnamed(alloc::vec![self.json_to_value(p, variant.fields[0].ty.id)?])
|
||||
}
|
||||
(Some(p), _) => {
|
||||
let ids: Vec<u32> = variant.fields.iter().map(|f| f.ty.id).collect();
|
||||
|
||||
match self.unnamed(p, move |i| ids[i.min(ids.len() - 1)])?.value {
|
||||
ValueDef::Composite(c) => c,
|
||||
_ => unreachable!("unnamed always returns a composite"),
|
||||
}
|
||||
}
|
||||
(None, _) => {
|
||||
return Err(CodecError::Encode(format!(
|
||||
"variant {name} needs a payload"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Value::variant(name, composite))
|
||||
}
|
||||
|
||||
TypeDef::BitSequence(_) => Err(CodecError::Encode(
|
||||
"encoding a bit sequence is not supported".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn unnamed(
|
||||
&self,
|
||||
json: &serde_json::Value,
|
||||
ty_at: impl Fn(usize) -> u32,
|
||||
) -> Result<Value<()>, CodecError> {
|
||||
let items = match json {
|
||||
serde_json::Value::Array(a) => a,
|
||||
_ => {
|
||||
return Err(CodecError::Encode(format!(
|
||||
"expected an array, got {json}"
|
||||
)))
|
||||
}
|
||||
};
|
||||
|
||||
let mut out = Vec::with_capacity(items.len());
|
||||
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
out.push(self.json_to_value(item, ty_at(i))?);
|
||||
}
|
||||
|
||||
Ok(Value::unnamed_composite(out))
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a JSON value as a SCALE primitive.
|
||||
///
|
||||
/// Numbers arrive as JSON numbers when they fit and as **decimal strings** when
|
||||
/// they do not: a `u128` balance loses precision above 2^53 in JSON, and this
|
||||
/// chain's balances are 12 decimal places, so that boundary is reached by
|
||||
/// ordinary amounts rather than exotic ones.
|
||||
fn primitive(
|
||||
json: &serde_json::Value,
|
||||
p: &scale_info::TypeDefPrimitive,
|
||||
) -> Result<Value<()>, CodecError> {
|
||||
use scale_info::TypeDefPrimitive as P;
|
||||
|
||||
let as_u128 = || -> Result<u128, CodecError> {
|
||||
match json {
|
||||
serde_json::Value::Number(n) => n
|
||||
.as_u64()
|
||||
.map(u128::from)
|
||||
.ok_or_else(|| CodecError::Encode(format!("{n} is not a whole number"))),
|
||||
serde_json::Value::String(s) => {
|
||||
let s = s.trim();
|
||||
|
||||
if let Some(h) = s.strip_prefix("0x") {
|
||||
u128::from_str_radix(h, 16)
|
||||
} else {
|
||||
s.parse::<u128>()
|
||||
}
|
||||
.map_err(|e| CodecError::Encode(format!("{s} is not a number: {e}")))
|
||||
}
|
||||
_ => Err(CodecError::Encode(format!("{json} is not a number"))),
|
||||
}
|
||||
};
|
||||
|
||||
Ok(match p {
|
||||
P::Bool => Value {
|
||||
value: ValueDef::Primitive(Primitive::Bool(json.as_bool().ok_or_else(|| {
|
||||
CodecError::Encode(format!("{json} is not a boolean"))
|
||||
})?)),
|
||||
context: (),
|
||||
},
|
||||
P::Str => Value {
|
||||
value: ValueDef::Primitive(Primitive::String(
|
||||
json.as_str()
|
||||
.ok_or_else(|| CodecError::Encode(format!("{json} is not a string")))?
|
||||
.to_string(),
|
||||
)),
|
||||
context: (),
|
||||
},
|
||||
P::U8 | P::U16 | P::U32 | P::U64 | P::U128 | P::U256 => Value {
|
||||
value: ValueDef::Primitive(Primitive::U128(as_u128()?)),
|
||||
context: (),
|
||||
},
|
||||
P::I8 | P::I16 | P::I32 | P::I64 | P::I128 | P::I256 => Value {
|
||||
value: ValueDef::Primitive(Primitive::I128(as_u128()? as i128)),
|
||||
context: (),
|
||||
},
|
||||
P::Char => Err(CodecError::Encode("char is not encodable".to_string()))?,
|
||||
})
|
||||
}
|
||||
|
||||
/// `0x…` as bytes, if this is a hex string.
|
||||
fn hex_bytes(json: &serde_json::Value) -> Option<Vec<u8>> {
|
||||
let s = json.as_str()?.strip_prefix("0x")?;
|
||||
|
||||
if s.len() % 2 != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
(0..s.len() / 2)
|
||||
.map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn byte_composite(bytes: &[u8]) -> Value<()> {
|
||||
Value::unnamed_composite(bytes.iter().map(|b| Value {
|
||||
value: ValueDef::Primitive(Primitive::U128(u128::from(*b))),
|
||||
context: (),
|
||||
}))
|
||||
}
|
||||
266
packages/quantus-codec/src/rs/runtime.rs
Normal file
266
packages/quantus-codec/src/rs/runtime.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
// Copyright 2026 @quantus/codec authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! The runtime's description of itself, and the handful of things this crate
|
||||
//! needs to look up in it.
|
||||
//!
|
||||
//! Every type id here is *read* from the metadata. Nothing in this file names a
|
||||
//! pallet, a call, a signed extension or a signature scheme, which is what lets
|
||||
//! it keep working across a runtime upgrade that changes any of them.
|
||||
|
||||
use alloc::collections::{BTreeMap, BTreeSet};
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use frame_metadata::v14::RuntimeMetadataV14;
|
||||
use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed};
|
||||
use parity_scale_codec::Decode;
|
||||
|
||||
/// Failures reading a runtime's description, or its data.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CodecError {
|
||||
/// The blob is not SCALE-encoded prefixed metadata.
|
||||
Malformed,
|
||||
/// Metadata this crate does not read. v14 is what every Quantus runtime
|
||||
/// observed so far emits; a chain that moves to v15/v16 needs this widened
|
||||
/// deliberately rather than silently mis-read.
|
||||
UnsupportedVersion(u8),
|
||||
/// The runtime does not describe its extrinsic in the usual shape.
|
||||
NoExtrinsicTypes,
|
||||
/// No such pallet, or no such call in it.
|
||||
NoSuchCall(String),
|
||||
/// A value did not match the type the registry said it would.
|
||||
Decode(String),
|
||||
/// A value could not be encoded as the type the registry declares.
|
||||
Encode(String),
|
||||
/// A signed extension declares a non-empty type and the caller supplied no
|
||||
/// value for it. Deliberately fatal — see [`crate::encode`].
|
||||
MissingExtension(String),
|
||||
/// The runtime declares no such storage entry, or not in that shape.
|
||||
NoStorageEntry(String),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for CodecError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Malformed => write!(f, "metadata did not decode"),
|
||||
Self::UnsupportedVersion(v) => write!(f, "unsupported metadata version {v}"),
|
||||
Self::NoExtrinsicTypes => write!(f, "runtime describes no extrinsic type"),
|
||||
Self::NoSuchCall(s) => write!(f, "no such call: {s}"),
|
||||
Self::Decode(s) => write!(f, "decoding against the registry failed: {s}"),
|
||||
Self::Encode(s) => write!(f, "encoding against the registry failed: {s}"),
|
||||
Self::NoStorageEntry(s) => write!(f, "no such storage entry: {s}"),
|
||||
Self::MissingExtension(s) => write!(
|
||||
f,
|
||||
"signed extension {s} declares a non-empty type and no value was supplied"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The four type parameters of the extrinsic envelope.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ExtrinsicTypes {
|
||||
pub address: u32,
|
||||
pub signature: u32,
|
||||
pub extra: u32,
|
||||
pub call: u32,
|
||||
}
|
||||
|
||||
/// One signed extension, as the runtime declares it.
|
||||
///
|
||||
/// `ty` is what it contributes to the extrinsic; `additional` is what it
|
||||
/// contributes to the signed payload but *not* to the extrinsic. Both are read
|
||||
/// from the metadata, in the order the runtime applies them, because that order
|
||||
/// is the payload's byte order.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExtensionDef {
|
||||
pub identifier: String,
|
||||
pub ty: u32,
|
||||
pub additional: u32,
|
||||
}
|
||||
|
||||
/// A runtime, as described by its own metadata.
|
||||
pub struct Runtime {
|
||||
pub(crate) metadata: RuntimeMetadataV14,
|
||||
pub(crate) extrinsic: ExtrinsicTypes,
|
||||
pub(crate) extensions: Vec<ExtensionDef>,
|
||||
/// Call type id per pallet name, so `encode_call` need not walk the pallet
|
||||
/// list for every argument.
|
||||
pub(crate) calls: BTreeMap<String, u32>,
|
||||
/// Registry types that are account ids.
|
||||
///
|
||||
/// Found by their **path**, not their length: a block hash is also 32 bytes,
|
||||
/// and rendering one as an address would be a lie. Held so that decoding can
|
||||
/// turn them into something a person can check against what they expected to
|
||||
/// see, rather than 32 bytes of hex nobody reads.
|
||||
pub(crate) account_tys: BTreeSet<u32>,
|
||||
/// The SS58 prefix to render account ids at, when one has been set.
|
||||
pub(crate) ss58_format: Option<u16>,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
/// Parse metadata exactly as `state_getMetadata` returns it.
|
||||
///
|
||||
/// That RPC takes a block hash and makes the node run `Metadata_metadata`
|
||||
/// against the runtime code in *that block's* state, so what arrives here is
|
||||
/// the runtime WASM describing itself, executed by the node. It is the only
|
||||
/// oracle on this chain that cannot go stale.
|
||||
pub fn from_metadata(raw: &[u8]) -> Result<Self, CodecError> {
|
||||
let prefixed =
|
||||
RuntimeMetadataPrefixed::decode(&mut &raw[..]).map_err(|_| CodecError::Malformed)?;
|
||||
let metadata = match prefixed.1 {
|
||||
RuntimeMetadata::V14(v) => v,
|
||||
other => return Err(CodecError::UnsupportedVersion(version_of(&other))),
|
||||
};
|
||||
|
||||
// The envelope's four parameters, by the names `UncheckedExtrinsic`
|
||||
// gives them. Read from the registry rather than assumed, which is the
|
||||
// whole point: `Signature` here is
|
||||
// `qp_dilithium_crypto::types::DilithiumSignatureScheme`, and no decoder
|
||||
// written against vanilla Substrate would guess that.
|
||||
let extrinsic = metadata
|
||||
.types
|
||||
.resolve(metadata.extrinsic.ty.id)
|
||||
.and_then(|e| {
|
||||
let param = |name: &str| {
|
||||
e.type_params
|
||||
.iter()
|
||||
.find(|p| p.name == name)
|
||||
.and_then(|p| p.ty)
|
||||
.map(|t| t.id)
|
||||
};
|
||||
Some(ExtrinsicTypes {
|
||||
address: param("Address")?,
|
||||
signature: param("Signature")?,
|
||||
extra: param("Extra")?,
|
||||
call: param("Call")?,
|
||||
})
|
||||
})
|
||||
.ok_or(CodecError::NoExtrinsicTypes)?;
|
||||
|
||||
let extensions = metadata
|
||||
.extrinsic
|
||||
.signed_extensions
|
||||
.iter()
|
||||
.map(|e| ExtensionDef {
|
||||
identifier: e.identifier.to_string(),
|
||||
ty: e.ty.id,
|
||||
additional: e.additional_signed.id,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let calls = metadata
|
||||
.pallets
|
||||
.iter()
|
||||
.filter_map(|p| p.calls.as_ref().map(|c| (p.name.to_string(), c.ty.id)))
|
||||
.collect();
|
||||
|
||||
let account_tys = metadata
|
||||
.types
|
||||
.types
|
||||
.iter()
|
||||
.filter(|t| {
|
||||
t.ty.path
|
||||
.segments
|
||||
.last()
|
||||
.is_some_and(|s| s == "AccountId32")
|
||||
})
|
||||
.map(|t| t.id)
|
||||
.collect();
|
||||
|
||||
Ok(Self {
|
||||
account_tys,
|
||||
calls,
|
||||
extensions,
|
||||
extrinsic,
|
||||
metadata,
|
||||
ss58_format: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Render account ids as SS58 at this prefix when decoding.
|
||||
///
|
||||
/// Off until set, because the prefix is a property of the chain a caller is
|
||||
/// talking to rather than of the metadata, and guessing it would put a
|
||||
/// plausible, wrong address in front of somebody about to approve a transfer.
|
||||
pub fn set_ss58_format(&mut self, prefix: u16) {
|
||||
self.ss58_format = Some(prefix);
|
||||
}
|
||||
|
||||
/// The extrinsic format version the metadata declares.
|
||||
///
|
||||
/// Not to be confused with the preamble byte of any particular extrinsic —
|
||||
/// see [`crate::decode::decode_extrinsic`], which is where that distinction
|
||||
/// has teeth.
|
||||
pub fn extrinsic_version(&self) -> u8 {
|
||||
self.metadata.extrinsic.version
|
||||
}
|
||||
|
||||
/// The signed extensions, in the order the runtime applies them.
|
||||
pub fn extensions(&self) -> &[ExtensionDef] {
|
||||
&self.extensions
|
||||
}
|
||||
|
||||
pub fn extrinsic_types(&self) -> ExtrinsicTypes {
|
||||
self.extrinsic
|
||||
}
|
||||
|
||||
pub(crate) fn types(&self) -> &scale_info::PortableRegistry {
|
||||
&self.metadata.types
|
||||
}
|
||||
|
||||
/// Whether a registry type encodes to nothing at all.
|
||||
///
|
||||
/// The question [`crate::encode`] asks of every signed extension: a
|
||||
/// zero-sized one contributes no bytes and needs no value from the caller,
|
||||
/// and anything else does. Answering it from the registry rather than from a
|
||||
/// list of known extension names is the difference between this crate and
|
||||
/// the thing it replaces.
|
||||
pub(crate) fn is_empty_ty(&self, id: u32) -> bool {
|
||||
match self.metadata.types.resolve(id).map(|t| &t.type_def) {
|
||||
// The unit type, and a tuple of nothing, are the same thing here.
|
||||
Some(scale_info::TypeDef::Tuple(t)) => {
|
||||
t.fields.iter().all(|f| self.is_empty_ty(f.id))
|
||||
}
|
||||
Some(scale_info::TypeDef::Composite(c)) => {
|
||||
c.fields.iter().all(|f| self.is_empty_ty(f.ty.id))
|
||||
}
|
||||
Some(scale_info::TypeDef::Array(a)) => {
|
||||
a.len == 0 || self.is_empty_ty(a.type_param.id)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn version_of(md: &RuntimeMetadata) -> u8 {
|
||||
match md {
|
||||
RuntimeMetadata::V14(_) => 14,
|
||||
RuntimeMetadata::V15(_) => 15,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
/// [`Runtime::is_empty_ty`], for the bindings module.
|
||||
pub fn is_empty_ty_pub(&self, id: u32) -> bool {
|
||||
self.is_empty_ty(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
/// [`Runtime::types`], for tests.
|
||||
pub fn types_pub(&self) -> &scale_info::PortableRegistry {
|
||||
self.types()
|
||||
}
|
||||
|
||||
/// [`Runtime::json_to_value`], for tests.
|
||||
pub fn json_to_value_pub(
|
||||
&self,
|
||||
json: &serde_json::Value,
|
||||
ty: u32,
|
||||
) -> Result<scale_value::Value<()>, CodecError> {
|
||||
self.json_to_value(json, ty)
|
||||
}
|
||||
}
|
||||
51
packages/quantus-codec/src/rs/ss58.rs
Normal file
51
packages/quantus-codec/src/rs/ss58.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 @quantus/codec authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! SS58, so a decoded call names an address a person can recognise.
|
||||
//!
|
||||
//! An account id is 32 bytes. Printed as hex it is a correct description of the
|
||||
//! value and useless to somebody being asked to check who they are paying —
|
||||
//! which is the only moment in a wallet where reading the recipient matters.
|
||||
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use blake2::digest::consts::U64;
|
||||
use blake2::{Blake2b, Digest};
|
||||
|
||||
/// The domain separator Substrate hashes into every SS58 checksum.
|
||||
const PREFIX: &[u8] = b"SS58PRE";
|
||||
|
||||
/// Encode an account id at a network prefix.
|
||||
///
|
||||
/// Prefixes below 64 are one byte; the rest are two, with the low six bits of
|
||||
/// the first byte and the high two bits arranged as Substrate specifies. Quantus
|
||||
/// is 189, so it takes the two-byte form — getting that wrong yields an address
|
||||
/// that looks right and belongs to nobody.
|
||||
pub fn encode(prefix: u16, account: &[u8]) -> String {
|
||||
let mut body = match prefix {
|
||||
0..=63 => alloc::vec![prefix as u8],
|
||||
64..=16_383 => {
|
||||
let low = ((prefix & 0b0000_0000_1111_1100) as u8) >> 2;
|
||||
let high = (((prefix >> 8) as u8) | ((prefix & 0b0000_0000_0000_0011) as u8) << 6) as u8;
|
||||
|
||||
alloc::vec![low | 0b0100_0000, high]
|
||||
}
|
||||
// Reserved by the specification; nothing should ask for one.
|
||||
_ => alloc::vec![0b0100_0000, 0],
|
||||
};
|
||||
|
||||
body.extend_from_slice(account);
|
||||
|
||||
let mut hasher = Blake2b::<U64>::new();
|
||||
|
||||
hasher.update(PREFIX);
|
||||
hasher.update(&body);
|
||||
|
||||
let checksum = hasher.finalize();
|
||||
let mut out: Vec<u8> = body;
|
||||
|
||||
out.extend_from_slice(&checksum[..2]);
|
||||
|
||||
bs58::encode(out).into_string()
|
||||
}
|
||||
225
packages/quantus-codec/src/rs/storage.rs
Normal file
225
packages/quantus-codec/src/rs/storage.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
// Copyright 2026 @quantus/codec authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Addressing chain state, using the runtime's own description of where it lives.
|
||||
//!
|
||||
//! A storage key is `twox128(pallet) ‖ twox128(item)`, then each map key hashed
|
||||
//! by the hasher the entry declares. None of those choices are known here:
|
||||
//! the pallet prefix, the item name, the hashers, the key type and the value
|
||||
//! type all come out of the metadata, so a runtime upgrade that re-hashes a map
|
||||
//! or changes a value's shape is followed rather than mis-read.
|
||||
|
||||
use alloc::format;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use frame_metadata::v14::{StorageEntryType, StorageHasher};
|
||||
|
||||
use crate::runtime::{CodecError, Runtime};
|
||||
|
||||
/// Where a storage value lives and what it decodes as.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StorageTarget {
|
||||
/// The full key, ready for `state_getStorage`.
|
||||
pub key: Vec<u8>,
|
||||
/// The registry type its value decodes as.
|
||||
pub value_ty: u32,
|
||||
/// What the chain means when it returns nothing.
|
||||
///
|
||||
/// `Some(bytes)` for a `Default` entry — an unfunded account reads as a zero
|
||||
/// balance, not as an error. `None` for an `Optional` entry, where nothing
|
||||
/// means nothing. Conflating the two is how a wallet reports "failed to load"
|
||||
/// for an account that simply has no money in it.
|
||||
pub default: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
/// Resolve a storage entry, hashing any map keys as the runtime declares.
|
||||
///
|
||||
/// `keys` are JSON, interpreted against the key types the metadata gives —
|
||||
/// so an `AccountId32` is the hex string its inner array accepts, and a
|
||||
/// double map takes two values in the order the entry lists its hashers.
|
||||
pub fn storage_target(
|
||||
&self,
|
||||
pallet: &str,
|
||||
item: &str,
|
||||
keys: &[serde_json::Value],
|
||||
) -> Result<StorageTarget, CodecError> {
|
||||
let entry = self
|
||||
.metadata
|
||||
.pallets
|
||||
.iter()
|
||||
.find(|p| p.name == pallet)
|
||||
.and_then(|p| p.storage.as_ref())
|
||||
.and_then(|s| s.entries.iter().find(|e| e.name == item))
|
||||
.ok_or_else(|| CodecError::NoStorageEntry(format!("{pallet}::{item}")))?;
|
||||
|
||||
let prefix = self
|
||||
.metadata
|
||||
.pallets
|
||||
.iter()
|
||||
.find(|p| p.name == pallet)
|
||||
.and_then(|p| p.storage.as_ref())
|
||||
.map(|s| s.prefix.clone())
|
||||
.unwrap_or_else(|| pallet.to_string());
|
||||
|
||||
let mut key = twox_128(prefix.as_bytes()).to_vec();
|
||||
|
||||
key.extend_from_slice(&twox_128(item.as_bytes()));
|
||||
|
||||
let value_ty = match &entry.ty {
|
||||
StorageEntryType::Plain(ty) => {
|
||||
if !keys.is_empty() {
|
||||
return Err(CodecError::NoStorageEntry(format!(
|
||||
"{pallet}::{item} takes no keys"
|
||||
)));
|
||||
}
|
||||
|
||||
ty.id
|
||||
}
|
||||
StorageEntryType::Map {
|
||||
hashers,
|
||||
key: key_ty,
|
||||
value,
|
||||
} => {
|
||||
if hashers.len() != keys.len() {
|
||||
return Err(CodecError::NoStorageEntry(format!(
|
||||
"{pallet}::{item} takes {} key(s), {} given",
|
||||
hashers.len(),
|
||||
keys.len()
|
||||
)));
|
||||
}
|
||||
|
||||
// One hasher means the declared key type *is* the key. More than
|
||||
// one means it is a tuple, one element per hasher, and the
|
||||
// elements are hashed separately rather than as a unit.
|
||||
let key_tys: Vec<u32> = if hashers.len() == 1 {
|
||||
alloc::vec![key_ty.id]
|
||||
} else {
|
||||
match self.types().resolve(key_ty.id).map(|t| &t.type_def) {
|
||||
Some(scale_info::TypeDef::Tuple(t)) => {
|
||||
t.fields.iter().map(|f| f.id).collect()
|
||||
}
|
||||
_ => {
|
||||
return Err(CodecError::NoStorageEntry(format!(
|
||||
"{pallet}::{item} has {} hashers and a non-tuple key",
|
||||
hashers.len()
|
||||
)))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for ((supplied, ty), hasher) in keys.iter().zip(key_tys).zip(hashers.iter()) {
|
||||
let value = self.json_to_value(supplied, ty)?;
|
||||
let mut encoded = Vec::new();
|
||||
|
||||
scale_value::scale::encode_as_type(&value, ty, self.types(), &mut encoded)
|
||||
.map_err(|e| CodecError::Encode(format!("{pallet}::{item} key: {e}")))?;
|
||||
|
||||
key.extend_from_slice(&hash_key(hasher, &encoded));
|
||||
}
|
||||
|
||||
value.id
|
||||
}
|
||||
};
|
||||
|
||||
let default = match &entry.modifier {
|
||||
frame_metadata::v14::StorageEntryModifier::Default => Some(entry.default.clone()),
|
||||
frame_metadata::v14::StorageEntryModifier::Optional => None,
|
||||
};
|
||||
|
||||
Ok(StorageTarget {
|
||||
default,
|
||||
key,
|
||||
value_ty,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decode a storage value against the type its entry declares.
|
||||
///
|
||||
/// `bytes` is what `state_getStorage` returned, or the entry's default when
|
||||
/// it returned nothing.
|
||||
pub fn decode_storage_value(
|
||||
&self,
|
||||
value_ty: u32,
|
||||
bytes: &[u8],
|
||||
) -> Result<serde_json::Value, CodecError> {
|
||||
let mut cursor = bytes;
|
||||
let value = self
|
||||
.decode_checked(value_ty, &mut cursor)
|
||||
.map_err(|e| CodecError::Decode(e))?;
|
||||
|
||||
if !cursor.is_empty() {
|
||||
return Err(CodecError::Decode(format!(
|
||||
"{} trailing bytes after storage value",
|
||||
cursor.len()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(self.render(&value))
|
||||
}
|
||||
}
|
||||
|
||||
/// `twox128`, as Substrate uses it for pallet and item prefixes.
|
||||
fn twox_128(input: &[u8]) -> [u8; 16] {
|
||||
use twox_hash::XxHash64;
|
||||
|
||||
let mut out = [0u8; 16];
|
||||
|
||||
out[..8].copy_from_slice(&XxHash64::oneshot(0, input).to_le_bytes());
|
||||
out[8..].copy_from_slice(&XxHash64::oneshot(1, input).to_le_bytes());
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
/// Hash one map key the way its entry declares.
|
||||
///
|
||||
/// The `Concat` variants keep the key after its hash, which is what makes a map
|
||||
/// enumerable. The plain variants do not.
|
||||
fn hash_key(hasher: &StorageHasher, encoded: &[u8]) -> Vec<u8> {
|
||||
use blake2::digest::consts::{U16, U32};
|
||||
use blake2::{Blake2b, Digest};
|
||||
use twox_hash::XxHash64;
|
||||
|
||||
match hasher {
|
||||
StorageHasher::Blake2_128 => Blake2b::<U16>::digest(encoded).to_vec(),
|
||||
StorageHasher::Blake2_256 => Blake2b::<U32>::digest(encoded).to_vec(),
|
||||
StorageHasher::Blake2_128Concat => {
|
||||
let mut v = Blake2b::<U16>::digest(encoded).to_vec();
|
||||
|
||||
v.extend_from_slice(encoded);
|
||||
v
|
||||
}
|
||||
StorageHasher::Twox128 => twox_128(encoded).to_vec(),
|
||||
StorageHasher::Twox256 => {
|
||||
let mut v = Vec::with_capacity(32);
|
||||
|
||||
for seed in 0..4u64 {
|
||||
v.extend_from_slice(&XxHash64::oneshot(seed, encoded).to_le_bytes());
|
||||
}
|
||||
|
||||
v
|
||||
}
|
||||
StorageHasher::Twox64Concat => {
|
||||
let mut v = XxHash64::oneshot(0, encoded).to_le_bytes().to_vec();
|
||||
|
||||
v.extend_from_slice(encoded);
|
||||
v
|
||||
}
|
||||
StorageHasher::Identity => encoded.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Hex, for the JSON boundary.
|
||||
pub(crate) fn hex(bytes: &[u8]) -> String {
|
||||
let mut s = String::with_capacity(2 + bytes.len() * 2);
|
||||
|
||||
s.push_str("0x");
|
||||
|
||||
for b in bytes {
|
||||
s.push(char::from_digit((b >> 4) as u32, 16).expect("nibble"));
|
||||
s.push(char::from_digit((b & 0x0f) as u32, 16).expect("nibble"));
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
328
packages/quantus-codec/src/rs/tests.rs
Normal file
328
packages/quantus-codec/src/rs/tests.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
// Copyright 2026 @quantus/codec authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Tested against metadata captured from the chain, not against a fixture this
|
||||
//! crate wrote. The point of the package is agreeing with a runtime; a test that
|
||||
//! agrees with itself proves nothing.
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::encode::{ExtensionValue, Supplied};
|
||||
use crate::runtime::Runtime;
|
||||
|
||||
/// Heisenberg at spec 148, `transactionVersion` 6 — the runtime quantus/extension#7
|
||||
/// tier 1 submits to.
|
||||
const HEISENBERG_V148: &str = include_str!("../../tests/heisenberg-v148.metadata.hex");
|
||||
|
||||
fn unhex(s: &str) -> Vec<u8> {
|
||||
let s = s.trim();
|
||||
let s = s.strip_prefix("0x").unwrap_or(s);
|
||||
|
||||
(0..s.len() / 2)
|
||||
.map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).expect("fixture is hex"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn heisenberg() -> Runtime {
|
||||
Runtime::from_metadata(&unhex(HEISENBERG_V148)).expect("fixture is v14 metadata")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_loads_and_describes_its_extrinsic() {
|
||||
let rt = heisenberg();
|
||||
|
||||
assert_eq!(rt.extrinsic_version(), 4);
|
||||
}
|
||||
|
||||
/// The listing that `@polkadot/api` cannot produce, and the reason this crate
|
||||
/// exists. Two of these extensions are Quantus-only and polkadot-js writes zero
|
||||
/// bytes for both halves of them by assumption; here the answer comes from the
|
||||
/// registry.
|
||||
#[test]
|
||||
fn every_signed_extension_is_read_from_the_registry() {
|
||||
let rt = heisenberg();
|
||||
let names: Vec<&str> = rt
|
||||
.extensions()
|
||||
.iter()
|
||||
.map(|e| e.identifier.as_str())
|
||||
.collect();
|
||||
|
||||
for e in rt.extensions() {
|
||||
println!(
|
||||
"{:<40} extra={:<5} additional={}",
|
||||
e.identifier,
|
||||
!rt.is_empty_ty_pub(e.ty),
|
||||
!rt.is_empty_ty_pub(e.additional)
|
||||
);
|
||||
}
|
||||
|
||||
// Not an exhaustive list on purpose — asserting the whole tuple would make
|
||||
// this test a second copy of the runtime, which is the mistake the crate is
|
||||
// here to avoid. These two are asserted because they are the ones no
|
||||
// Substrate-shaped decoder knows about.
|
||||
assert!(names.contains(&"ReversibleTransactionExtension"));
|
||||
assert!(names.contains(&"WormholeProofRecorderExtension"));
|
||||
}
|
||||
|
||||
/// The guarantee in [`Runtime::encode_extensions`]: a declared, non-empty
|
||||
/// extension with no supplied value refuses to produce a payload.
|
||||
#[test]
|
||||
fn a_missing_extension_value_is_an_error_not_a_short_payload() {
|
||||
let rt = heisenberg();
|
||||
let empty: BTreeMap<String, ExtensionValue> = BTreeMap::new();
|
||||
|
||||
let err = rt
|
||||
.encode_extensions(&empty)
|
||||
.expect_err("CheckSpecVersion declares a u32 additional; nothing supplied it");
|
||||
|
||||
assert!(
|
||||
err.to_string().contains("no value was supplied"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A `MultiAddress::Id` is a variant carrying a newtype around `[u8; 32]`, and
|
||||
/// the caller should be able to say so with a hex string and a variant name
|
||||
/// without knowing about either wrapper.
|
||||
#[test]
|
||||
fn an_account_id_encodes_from_its_hex() {
|
||||
let rt = heisenberg();
|
||||
let tys = rt.extrinsic_types();
|
||||
let id = "0x".to_string() + &"11".repeat(32);
|
||||
let json = serde_json::json!({ "Id": id });
|
||||
|
||||
let value = rt
|
||||
.json_to_value_pub(&json, tys.address)
|
||||
.expect("MultiAddress::Id from hex");
|
||||
|
||||
let mut out = Vec::new();
|
||||
|
||||
scale_value::scale::encode_as_type(&value, tys.address, rt.types_pub(), &mut out)
|
||||
.expect("encodes");
|
||||
|
||||
// Variant index 0 for `Id`, then 32 raw bytes with no length prefix.
|
||||
assert_eq!(out.len(), 33);
|
||||
assert_eq!(out[0], 0);
|
||||
assert_eq!(&out[1..], &[0x11u8; 32]);
|
||||
}
|
||||
|
||||
/// Pre-encoded bytes are accepted — a dapp encodes its own `era`, and nothing
|
||||
/// here knows the era algorithm — but they are round-tripped against the
|
||||
/// runtime's declared type rather than trusted.
|
||||
#[test]
|
||||
fn raw_extension_bytes_are_validated_not_trusted() {
|
||||
let rt = heisenberg();
|
||||
let mut values: BTreeMap<String, ExtensionValue> = BTreeMap::new();
|
||||
|
||||
let fill = |values: &mut BTreeMap<String, ExtensionValue>, era: Supplied| {
|
||||
values.insert(
|
||||
"CheckMortality".to_string(),
|
||||
ExtensionValue {
|
||||
additional: Some(Supplied::Json(serde_json::json!(
|
||||
"0x".to_string() + &"aa".repeat(32)
|
||||
))),
|
||||
extra: Some(era),
|
||||
},
|
||||
);
|
||||
values.insert(
|
||||
"CheckNonce".to_string(),
|
||||
ExtensionValue {
|
||||
additional: None,
|
||||
extra: Some(Supplied::Json(serde_json::json!(1))),
|
||||
},
|
||||
);
|
||||
values.insert(
|
||||
"ChargeTransactionPayment".to_string(),
|
||||
ExtensionValue {
|
||||
additional: None,
|
||||
extra: Some(Supplied::Json(serde_json::json!(0))),
|
||||
},
|
||||
);
|
||||
values.insert(
|
||||
"CheckMetadataHash".to_string(),
|
||||
ExtensionValue {
|
||||
additional: Some(Supplied::Json(serde_json::json!("None"))),
|
||||
extra: Some(Supplied::Json(serde_json::json!("Disabled"))),
|
||||
},
|
||||
);
|
||||
for (id, v) in [
|
||||
("CheckSpecVersion", 148),
|
||||
("CheckTxVersion", 6),
|
||||
] {
|
||||
values.insert(
|
||||
id.to_string(),
|
||||
ExtensionValue {
|
||||
additional: Some(Supplied::Json(serde_json::json!(v))),
|
||||
extra: None,
|
||||
},
|
||||
);
|
||||
}
|
||||
values.insert(
|
||||
"CheckGenesis".to_string(),
|
||||
ExtensionValue {
|
||||
additional: Some(Supplied::Json(serde_json::json!(
|
||||
"0x".to_string() + &"bb".repeat(32)
|
||||
))),
|
||||
extra: None,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
// `0x00` is Era::Immortal, and it round-trips.
|
||||
fill(&mut values, Supplied::Raw(alloc::vec![0x00]));
|
||||
|
||||
let encoded = rt.encode_extensions(&values).expect("immortal era encodes");
|
||||
|
||||
assert_eq!(encoded.extra[0], 0x00);
|
||||
|
||||
// Two bytes where the type says one is a disagreement about the format. A
|
||||
// signer that appended them would produce a signature the chain rejects as
|
||||
// BadProof, with nothing locally to say why.
|
||||
fill(&mut values, Supplied::Raw(alloc::vec![0x00, 0x00]));
|
||||
|
||||
let err = rt
|
||||
.encode_extensions(&values)
|
||||
.expect_err("trailing byte is refused");
|
||||
|
||||
assert!(
|
||||
err.to_string().contains("trailing"),
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A call nested inside another call — `Utility.batch_all` carries a
|
||||
/// `Vec<RuntimeCall>`, so each element is a call spelled exactly as a top-level
|
||||
/// one. Anything less than that and batching has to be written positionally,
|
||||
/// which is unreadable and silently order-dependent.
|
||||
#[test]
|
||||
fn a_call_nests_inside_another_call() {
|
||||
let rt = heisenberg();
|
||||
let dest = "0x".to_string() + &"22".repeat(32);
|
||||
let one = serde_json::json!({
|
||||
"Balances": { "transfer_keep_alive": { "dest": { "Id": dest }, "value": "1000000000" } }
|
||||
});
|
||||
|
||||
let encoded = rt
|
||||
.encode_call(
|
||||
"Utility",
|
||||
"batch_all",
|
||||
&serde_json::json!({ "calls": [one.clone(), one] }),
|
||||
)
|
||||
.expect("batch_all of two transfers encodes");
|
||||
|
||||
// Over 256 bytes, which is where Substrate's signing rule switches to
|
||||
// BLAKE2b — the branch quantus/extension#7 wants exercised.
|
||||
assert!(encoded.len() > 80, "unexpectedly short: {}", encoded.len());
|
||||
|
||||
let decoded = rt.decode_call(&encoded).expect("and decodes again");
|
||||
let calls = decoded["Utility"]["batch_all"]["calls"]
|
||||
.as_array()
|
||||
.expect("calls is a list");
|
||||
|
||||
assert_eq!(calls.len(), 2);
|
||||
assert_eq!(
|
||||
calls[0]["Balances"]["transfer_keep_alive"]["value"],
|
||||
serde_json::json!("1000000000")
|
||||
);
|
||||
}
|
||||
|
||||
/// `System::Account` is the entry every wallet needs first, and it is a map with
|
||||
/// a `Blake2_128Concat` hasher over an `AccountId32`. Nothing here says so — the
|
||||
/// hasher and both types come out of the metadata.
|
||||
#[test]
|
||||
fn a_storage_key_is_built_from_the_declared_hasher() {
|
||||
let rt = heisenberg();
|
||||
let who = "0x".to_string() + &"11".repeat(32);
|
||||
|
||||
let target = rt
|
||||
.storage_target("System", "Account", &[serde_json::json!(who)])
|
||||
.expect("System::Account is a map over AccountId32");
|
||||
|
||||
// twox128(prefix) ++ twox128(item) ++ blake2_128(key) ++ key
|
||||
assert_eq!(target.key.len(), 16 + 16 + 16 + 32);
|
||||
assert_eq!(&target.key[48..], &[0x11u8; 32]);
|
||||
|
||||
// AccountInfo is a `Default` entry: an account nobody has ever funded reads
|
||||
// as a zero balance, not as a missing value. A wallet that treated the two
|
||||
// alike would report a failure for an empty account.
|
||||
let default = target.default.expect("AccountInfo is a Default entry");
|
||||
let decoded = rt
|
||||
.decode_storage_value(target.value_ty, &default)
|
||||
.expect("the declared default decodes as the declared type");
|
||||
|
||||
// Every integer renders as a decimal string, whatever its width — see
|
||||
// `decode::render` for why the width is not available to switch on.
|
||||
assert_eq!(decoded["nonce"], serde_json::json!("0"));
|
||||
assert_eq!(decoded["data"]["free"], serde_json::json!("0"));
|
||||
}
|
||||
|
||||
/// The number of keys is the runtime's to state, not the caller's to assume.
|
||||
#[test]
|
||||
fn a_storage_entry_refuses_the_wrong_number_of_keys() {
|
||||
let rt = heisenberg();
|
||||
|
||||
assert!(rt.storage_target("System", "Account", &[]).is_err());
|
||||
assert!(rt.storage_target("System", "Number", &[serde_json::json!(1)]).is_err());
|
||||
assert!(rt.storage_target("System", "NoSuchThing", &[]).is_err());
|
||||
}
|
||||
|
||||
/// The recipient of a transfer must render as an address somebody can check
|
||||
/// against what they meant to type. 32 bytes of hex is a correct description of
|
||||
/// the value and the one thing nobody reads.
|
||||
///
|
||||
/// The vector is crystal_bob on Heisenberg, taken from the chain rather than
|
||||
/// computed here.
|
||||
#[test]
|
||||
fn an_account_id_in_a_call_renders_as_ss58() {
|
||||
let mut rt = heisenberg();
|
||||
let bob_id = "0x300bb607ba60e89461d2f9005668231ceb30237b33db53a614164b8590965519";
|
||||
const BOB: &str = "qzkYEQv8tQsmniZYdame3Cku18RL5g9bGK9Pdydq5TMPdpE3y";
|
||||
|
||||
let call = rt
|
||||
.encode_call(
|
||||
"Balances",
|
||||
"transfer_keep_alive",
|
||||
&serde_json::json!({ "dest": { "Id": bob_id }, "value": "1000000000" }),
|
||||
)
|
||||
.expect("a transfer to bob");
|
||||
|
||||
// Until a prefix is set, hex. The prefix belongs to the chain a caller is
|
||||
// talking to, not to the metadata, and a guessed one puts a plausible wrong
|
||||
// address in front of somebody about to approve a transfer.
|
||||
let raw = rt.decode_call(&call).expect("decodes");
|
||||
|
||||
assert_eq!(
|
||||
raw["Balances"]["transfer_keep_alive"]["dest"]["Id"],
|
||||
serde_json::json!(bob_id)
|
||||
);
|
||||
|
||||
rt.set_ss58_format(189);
|
||||
|
||||
let decoded = rt.decode_call(&call).expect("decodes again");
|
||||
|
||||
assert_eq!(
|
||||
decoded["Balances"]["transfer_keep_alive"]["dest"]["Id"],
|
||||
serde_json::json!(BOB)
|
||||
);
|
||||
}
|
||||
|
||||
/// Found by registry path, not by length. A block hash is 32 bytes too, and
|
||||
/// rendering one as an address would be a lie a reader cannot catch.
|
||||
#[test]
|
||||
fn a_32_byte_value_that_is_not_an_account_stays_hex() {
|
||||
let mut rt = heisenberg();
|
||||
|
||||
rt.set_ss58_format(189);
|
||||
|
||||
let target = rt
|
||||
.storage_target("System", "BlockHash", &[serde_json::json!(0)])
|
||||
.expect("System::BlockHash is a map over block number");
|
||||
let hash = [0x11u8; 32];
|
||||
let decoded = rt
|
||||
.decode_storage_value(target.value_ty, &hash)
|
||||
.expect("a block hash decodes");
|
||||
|
||||
assert_eq!(decoded, serde_json::json!("0x".to_string() + &"11".repeat(32)));
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
18
packages/quantus-codec/tsconfig.build.json
Normal file
18
packages/quantus-codec/tsconfig.build.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "..",
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src",
|
||||
"emitDeclarationOnly": false
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"references": []
|
||||
}
|
||||
955
packages/quantus-crypto/Cargo.lock
generated
Normal file
955
packages/quantus-crypto/Cargo.lock
generated
Normal file
@@ -0,0 +1,955 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bip39"
|
||||
version = "2.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc"
|
||||
dependencies = [
|
||||
"bitcoin_hashes",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitcoin_hashes"
|
||||
version = "0.14.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2"
|
||||
dependencies = [
|
||||
"hex-conservative",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
||||
|
||||
[[package]]
|
||||
name = "fixed-hash"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c"
|
||||
dependencies = [
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hex-conservative"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db3fef046dca3ca91ee1408a8c1b80ab777e80a4d308d1bf4e7adb3fcb047e08"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex-literal"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.105"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keccak-hash"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce2bd4c29270e724d3eaadf7bdc8700af4221fc0ed771b855eadcd1b98d52851"
|
||||
dependencies = [
|
||||
"primitive-types",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.189"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-iter",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-complex"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-iter"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-rational"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
|
||||
dependencies = [
|
||||
"num-bigint",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
dependencies = [
|
||||
"critical-section",
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "plonky2_maybe_rayon"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1e554181dc95243b8d9948ae7bae5759c7fb2502fed28f671f95ef38079406"
|
||||
|
||||
[[package]]
|
||||
name = "plonky2_util"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c32c137808ca984ab2458b612b7eb0462d853ee041a3136e83d54b96074c7610"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "primitive-types"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373"
|
||||
dependencies = [
|
||||
"fixed-hash",
|
||||
"uint",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qp-plonky2"
|
||||
version = "1.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd331d489a309f88e2d0e35a2b996932c7d92038b91ccc656a0a8e6b11b6977"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
"critical-section",
|
||||
"hashbrown",
|
||||
"itertools",
|
||||
"keccak-hash",
|
||||
"log",
|
||||
"num",
|
||||
"once_cell",
|
||||
"plonky2_maybe_rayon",
|
||||
"plonky2_util",
|
||||
"qp-plonky2-core",
|
||||
"qp-plonky2-field",
|
||||
"qp-plonky2-verifier",
|
||||
"qp-poseidon-core",
|
||||
"rand 0.10.1",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"unroll",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qp-plonky2-core"
|
||||
version = "1.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b81a3a9fce99f7bd45b8578f8d9b6a33507d34c2eb2c47c969b464db1ad601d3"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
"hashbrown",
|
||||
"itertools",
|
||||
"keccak-hash",
|
||||
"log",
|
||||
"num",
|
||||
"plonky2_util",
|
||||
"qp-plonky2-field",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"unroll",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qp-plonky2-field"
|
||||
version = "1.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1630d418ddce9feba18d3364596711d07074851757301350de313eef0a31af4f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"num",
|
||||
"plonky2_util",
|
||||
"rustc_version",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"unroll",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qp-plonky2-verifier"
|
||||
version = "1.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "944da5dec21ee476d561f6c38caddf45e829f3cc5fccbc76f6ece03660378dbe"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
"critical-section",
|
||||
"hashbrown",
|
||||
"itertools",
|
||||
"keccak-hash",
|
||||
"log",
|
||||
"num",
|
||||
"once_cell",
|
||||
"plonky2_util",
|
||||
"qp-plonky2-core",
|
||||
"qp-plonky2-field",
|
||||
"qp-poseidon-core",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"unroll",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qp-poseidon-core"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5872607e25ea4ee5fb37e64bf1462168e1a36a4e719cdc8a105533c708253918"
|
||||
|
||||
[[package]]
|
||||
name = "qp-rusty-crystals-dilithium"
|
||||
version = "4.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "789877c169226a35d2ea686bbd9d506becc693f7bb0ee91acc03f74491e80c0f"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qp-rusty-crystals-hdwallet"
|
||||
version = "4.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51ec6c3db4055c217a503c45d0c101cf3c10d4fc1e562f0588aa55dda4f60a4d"
|
||||
dependencies = [
|
||||
"bip39",
|
||||
"getrandom 0.2.17",
|
||||
"hex",
|
||||
"hex-literal",
|
||||
"qp-poseidon-core",
|
||||
"qp-rusty-crystals-dilithium",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"thiserror",
|
||||
"unicode-normalization",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qp-wormhole-circuit"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55167daf965a3616b74184171148b94c64b3a48771d6433a1a43377a6e46c6e8"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"hex",
|
||||
"qp-plonky2",
|
||||
"qp-wormhole-inputs",
|
||||
"qp-zk-circuits-common",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qp-wormhole-inputs"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c07863a2211a17b46319289c5ef22e5670a3fb8653386bb366c418193254793"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qp-zk-circuits-common"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dec45510701f160fdb730bdb622d6cdd0a62547e607c9b1ecb19558202797f81"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"qp-plonky2",
|
||||
"qp-poseidon-core",
|
||||
"qp-wormhole-inputs",
|
||||
"rand 0.8.6",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quantus_crypto"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"qp-poseidon-core",
|
||||
"qp-rusty-crystals-dilithium",
|
||||
"qp-rusty-crystals-hdwallet",
|
||||
"qp-wormhole-circuit",
|
||||
"qp-zk-circuits-common",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"rand_core 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cf0ded5c4e56918d8f8a339e1bb67d038d3bc6d144ac407904015ba2e4cde9b"
|
||||
dependencies = [
|
||||
"tinyvec_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec_macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "uint"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crunchy",
|
||||
"hex",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unroll"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ad948c1cb799b1a70f836077721a92a35ac177d4daddf4c20a633786d4cf618"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.5",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
47
packages/quantus-crypto/Cargo.toml
Normal file
47
packages/quantus-crypto/Cargo.toml
Normal file
@@ -0,0 +1,47 @@
|
||||
# Quantus post-quantum crypto, compiled to WASM for the browser.
|
||||
#
|
||||
# Deliberately a separate crate from `wasm-crypto` rather than more files inside
|
||||
# it: that package is built with `nightly-2022-06-24` (see scripts/rust-version.sh)
|
||||
# against a 2019-era dependency set, and the ML-DSA crates use inline `const {}`
|
||||
# blocks that need Rust >= 1.79. The two cannot share a Cargo graph, and bumping
|
||||
# the older one would mean rewriting upstream's sr25519/ed25519 build — which is
|
||||
# the thing we most want to leave alone so rebases stay boring. See quantus/wasm#1.
|
||||
|
||||
[package]
|
||||
authors = ["Quantus Network Developers <hello@quantus.com>"]
|
||||
description = "WASM bindings to the Quantus chain's post-quantum crypto crates."
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
name = "quantus_crypto"
|
||||
publish = false
|
||||
repository = "https://git.lair.cafe/quantus/wasm"
|
||||
resolver = "2"
|
||||
version = "0.0.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
# The crates the runtime itself uses. Versions match quantus-apps/quantus_sdk's
|
||||
# rust bridge, which is the other non-Rust consumer of exactly this surface.
|
||||
qp-poseidon-core = "3.1.0"
|
||||
qp-rusty-crystals-dilithium = { version = "4.1.1", default-features = false, features = ["ml-dsa-65", "ml-dsa-87"] }
|
||||
qp-rusty-crystals-hdwallet = { version = "4.1.1", default-features = false, features = ["ml-dsa-65", "ml-dsa-87"] }
|
||||
wasm-bindgen = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
# The chain's own nullifier, for known-answer tests only. It pulls in plonky2,
|
||||
# which has no place in the shipped WASM: the port in rs/hdwallet.rs has to
|
||||
# agree with it, and these tests are how that is shown rather than asserted.
|
||||
qp-wormhole-circuit = { version = "=4.3.0", default-features = false, features = ["std"] }
|
||||
qp-zk-circuits-common = { version = "=4.3.0" }
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
debug = false
|
||||
debug-assertions = false
|
||||
incremental = false
|
||||
lto = true
|
||||
opt-level = "z"
|
||||
panic = "abort"
|
||||
rpath = false
|
||||
76
packages/quantus-crypto/README.md
Normal file
76
packages/quantus-crypto/README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# @quantus/crypto
|
||||
|
||||
Quantus post-quantum crypto for the browser: ML-DSA-65 and ML-DSA-87 signatures,
|
||||
Poseidon2-over-Goldilocks account-id hashing, and hardened BIP44 key derivation.
|
||||
|
||||
Every function delegates to the crates the Quantus runtime itself uses —
|
||||
`qp-rusty-crystals-dilithium`, `qp-poseidon-core`, `qp-rusty-crystals-hdwallet` —
|
||||
rather than reimplementing them. A browser wallet that disagreed with the chain
|
||||
about a key or a signature would produce perfectly well-formed output that the
|
||||
chain rejects, and nothing on this side could tell.
|
||||
|
||||
## Why a separate package from `@polkadot/wasm-crypto`
|
||||
|
||||
They cannot share a Cargo build. `wasm-crypto` is compiled with
|
||||
`nightly-2022-06-24` against a 2019-era dependency set; the ML-DSA crates use
|
||||
inline `const {}` blocks that require Rust >= 1.79. Modernising the older build
|
||||
would mean rewriting upstream's sr25519/ed25519 crypto, which is the thing most
|
||||
worth leaving untouched so rebases onto upstream stay boring.
|
||||
|
||||
What *is* shared is the packaging: the WASM is zlib-compressed and base64'd into
|
||||
the JS at build time, so nothing is fetched at runtime. That matters because the
|
||||
consumer is an MV3 service worker under `script-src 'self' 'wasm-unsafe-eval'`,
|
||||
which can compile WASM but cannot usefully fetch it, and because callers like
|
||||
`pair.sign()` are synchronous and have no `await` to give.
|
||||
|
||||
`@polkadot/wasm-bridge` is deliberately not used: its `Bridge` implements
|
||||
wasm-bindgen 0.2.79's JS-heap ABI, while this crate builds with 0.2.128, which
|
||||
uses externref tables. wasm-bindgen's own generated glue plus `initSync` is both
|
||||
smaller and correct.
|
||||
|
||||
The only runtime dependency is `fflate`, for zlib inflate. Base64 decoding is
|
||||
fifteen lines here rather than a dependency. Both were originally taken from
|
||||
`@polkadot/wasm-util`, which turned out to cost more than it saved: its index
|
||||
re-exports `packageDetect`, dragging in a `@polkadot/util` peer dependency for a
|
||||
side effect we do not want, and being a workspace package it resolved through its
|
||||
*own* repo's node_modules when this package was consumed by symlink from another
|
||||
checkout — which is exactly how `quantus/common` consumes it during development.
|
||||
|
||||
## Scheme selector
|
||||
|
||||
`Scheme.MlDsa87 = 0`, `Scheme.MlDsa65 = 1` — these are the chain's own
|
||||
`DilithiumSignatureScheme` variant indices, so the number threaded through this
|
||||
API is the byte that ends up on the wire. New accounts use ML-DSA-65; ML-DSA-87
|
||||
is legacy and must be supported but never chosen.
|
||||
|
||||
## Signing context
|
||||
|
||||
ML-DSA hashes a context into the signature. Quantus extrinsics on spec >= 148 are
|
||||
verified under `QUANTUS_EXTRINSIC`, earlier specs under the empty context. A
|
||||
signature made under the wrong one is cryptographically valid, rejected by the
|
||||
chain, and indistinguishable locally — so use `contextForSpec(specVersion)`
|
||||
rather than picking one by hand. Nothing here guesses on your behalf.
|
||||
|
||||
## Sizes come from the crate
|
||||
|
||||
`sizes(scheme)` returns the public/secret/signature lengths rather than exposing
|
||||
constants to copy. They are consensus-critical — the runtime decodes a fixed-size
|
||||
array with no compact length prefix — and a JS constant that drifted would
|
||||
re-frame every byte after the signature while looking entirely healthy.
|
||||
|
||||
## Building
|
||||
|
||||
```sh
|
||||
yarn install-build-deps # downloads wasm-bindgen 0.2.128 and binaryen
|
||||
./scripts/build-quantus.sh
|
||||
```
|
||||
|
||||
The Rust toolchain is pinned in `rust-toolchain.toml` to the same channel the
|
||||
chain builds its runtime with.
|
||||
|
||||
## Tests
|
||||
|
||||
`cargo test` runs conformance tests whose expected values come from the `quantus`
|
||||
CLI, not from this crate — the dev-genesis account ids, HD derivation at both
|
||||
schemes' default paths, and context separation. A test that pinned our own output
|
||||
would keep passing through exactly the drift they exist to catch.
|
||||
24
packages/quantus-crypto/package.json
Normal file
24
packages/quantus-crypto/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"author": "Quantus Network Developers <hello@quantus.com>",
|
||||
"bugs": "https://git.lair.cafe/quantus/wasm/issues",
|
||||
"description": "Quantus post-quantum crypto (ML-DSA, Poseidon2, HD derivation) for the browser",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"homepage": "https://git.lair.cafe/quantus/wasm/src/branch/main/packages/quantus-crypto#readme",
|
||||
"license": "Apache-2.0",
|
||||
"name": "@quantus/crypto",
|
||||
"repository": {
|
||||
"directory": "packages/quantus-crypto",
|
||||
"type": "git",
|
||||
"url": "https://git.lair.cafe/quantus/wasm.git"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"version": "0.3.0",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"fflate": "^0.8.2",
|
||||
"tslib": "^2.7.0"
|
||||
}
|
||||
}
|
||||
8
packages/quantus-crypto/rust-toolchain.toml
Normal file
8
packages/quantus-crypto/rust-toolchain.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
# Matches the chain's toolchain (chain/rust-toolchain), so this crate is built by
|
||||
# the same compiler that builds the runtime it has to agree with. Upstream's
|
||||
# `wasm-crypto` keeps its own nightly-2022-06-24 pin; the two builds are separate
|
||||
# on purpose. See quantus/wasm#1.
|
||||
[toolchain]
|
||||
channel = "1.93.0"
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
profile = "minimal"
|
||||
51
packages/quantus-crypto/src/base64.ts
Normal file
51
packages/quantus-crypto/src/base64.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
|
||||
// An array indexer rather than a Map: the input is ASCII by construction, so it
|
||||
// cannot overflow, and array access is measurably faster on the hot loop.
|
||||
const MAP = new Array<number>(256);
|
||||
|
||||
for (let i = 0; i < CHARS.length; i++) {
|
||||
MAP[CHARS.charCodeAt(i)] = i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode base64 into a caller-supplied buffer.
|
||||
*
|
||||
* Deliberately not `atob` or `Buffer.from`: the first is browser-only, the second
|
||||
* node-only, and this runs in an MV3 service worker, a Worker, node tests and a
|
||||
* bundled extension page. The output length is known at build time, so the
|
||||
* caller provides the buffer and there is no growth or reallocation.
|
||||
*
|
||||
* This is a reimplementation of `@polkadot/wasm-util`'s base64Decode, which was
|
||||
* the dependency it replaced. That package's index re-exports `packageDetect`,
|
||||
* dragging in a `@polkadot/util` peer dependency for a side effect we do not
|
||||
* want, and being a workspace package it resolved through its own repo's
|
||||
* node_modules when consumed by symlink from another checkout. Fifteen lines is
|
||||
* cheaper than either problem.
|
||||
*/
|
||||
export function base64Decode (data: string, out: Uint8Array): Uint8Array {
|
||||
let byte = 0;
|
||||
let bits = 0;
|
||||
let pos = 0;
|
||||
|
||||
for (let i = 0; i < data.length && pos < out.length; i++) {
|
||||
const value = MAP[data.charCodeAt(i)];
|
||||
|
||||
if (value === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte = (byte << 6) | value;
|
||||
bits += 6;
|
||||
|
||||
if (bits >= 8) {
|
||||
bits -= 8;
|
||||
out[pos++] = (byte >>> bits) & 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
6
packages/quantus-crypto/src/bytes.d.ts
vendored
Normal file
6
packages/quantus-crypto/src/bytes.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
// 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;
|
||||
10
packages/quantus-crypto/src/bytes.js
Normal file
10
packages/quantus-crypto/src/bytes.js
Normal file
@@ -0,0 +1,10 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Generated as part of the build, do not edit
|
||||
|
||||
export const lenIn = 0;
|
||||
|
||||
export const lenOut = 0;
|
||||
|
||||
export const bytes = '';
|
||||
210
packages/quantus-crypto/src/crypto.ts
Normal file
210
packages/quantus-crypto/src/crypto.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { ext_mldsa_derive, ext_mldsa_from_seed, ext_mldsa_is_scheme, ext_mldsa_sign, ext_mldsa_sizes, ext_mldsa_verify, ext_poseidon_hash, ext_wormhole_addresses, ext_wormhole_nullifiers } from './generated/quantus_crypto.js';
|
||||
import { initWasm } from './init.js';
|
||||
import { Scheme } from './scheme.js';
|
||||
|
||||
export interface Keypair {
|
||||
publicKey: Uint8Array;
|
||||
secretKey: Uint8Array;
|
||||
}
|
||||
|
||||
export interface Sizes {
|
||||
/** Public key length: 1952 for ML-DSA-65, 2592 for ML-DSA-87. */
|
||||
publicKey: number;
|
||||
/** Secret key length: 4032 / 4896. */
|
||||
secretKey: number;
|
||||
/** Signature length: 3309 / 4627. */
|
||||
signature: number;
|
||||
/** `signature ‖ publicKey`, the runtime's wire form: 5261 / 7219. */
|
||||
signatureWithPublicKey: number;
|
||||
}
|
||||
|
||||
function ready (): void {
|
||||
const error = initWasm();
|
||||
|
||||
if (error) {
|
||||
throw new Error(`@quantus/crypto: WASM unavailable: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Key and signature sizes for a scheme, read from the crate rather than
|
||||
* hardcoded here.
|
||||
*
|
||||
* These are consensus-critical — the runtime decodes a fixed-size array off the
|
||||
* wire — so a constant that drifted from the crate would re-frame every byte
|
||||
* after the signature while looking entirely healthy. Ask, don't assume.
|
||||
*/
|
||||
export function sizes (scheme: Scheme): Sizes {
|
||||
ready();
|
||||
|
||||
const raw = new DataView(ext_mldsa_sizes(scheme).buffer);
|
||||
|
||||
return {
|
||||
publicKey: raw.getUint32(0, true),
|
||||
secretKey: raw.getUint32(4, true),
|
||||
signature: raw.getUint32(8, true),
|
||||
signatureWithPublicKey: raw.getUint32(12, true)
|
||||
};
|
||||
}
|
||||
|
||||
/** Whether this build supports `scheme`. */
|
||||
export function isScheme (scheme: number): scheme is Scheme {
|
||||
ready();
|
||||
|
||||
return ext_mldsa_is_scheme(scheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* The account id for a public key — Poseidon2 over Goldilocks, 32 bytes out.
|
||||
*
|
||||
* This is the step that has no Substrate equivalent. There, an `AccountId32`
|
||||
* *is* the public key; here it is a one-way hash of it, which is why a Quantus
|
||||
* signature has to carry its public key inside itself and why nothing can
|
||||
* recover a key from an address.
|
||||
*/
|
||||
export function accountFromPublicKey (publicKey: Uint8Array): Uint8Array {
|
||||
ready();
|
||||
|
||||
return ext_poseidon_hash(publicKey);
|
||||
}
|
||||
|
||||
/** A keypair from 32 bytes of entropy — FIPS 204 `ML-DSA.KeyGen_internal`. */
|
||||
export function keypairFromSeed (seed: Uint8Array, scheme: Scheme): Keypair {
|
||||
ready();
|
||||
|
||||
return split(ext_mldsa_from_seed(seed, scheme), scheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* A keypair from a BIP39 mnemonic at a hardened Quantus derivation path.
|
||||
*
|
||||
* `path` must be hardened at every level — lattice keys have no public
|
||||
* derivability, so there is no soft-junction equivalent and one is rejected
|
||||
* rather than reinterpreted.
|
||||
*/
|
||||
export function keypairFromMnemonic (mnemonic: string, password: string, path: string, scheme: Scheme): Keypair {
|
||||
ready();
|
||||
|
||||
return split(ext_mldsa_derive(mnemonic, password, path, scheme), scheme);
|
||||
}
|
||||
|
||||
/** Which wormhole branch: where deposits arrive, or where a send returns its change. */
|
||||
export enum WormholeBranch {
|
||||
Receive = 0,
|
||||
Change = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Consecutive wormhole addresses for one account and branch, as 32-byte account
|
||||
* ids.
|
||||
*
|
||||
* Paths are `m/44'/189189189'/<account>'/<branch>'/<index>'`, the mobile
|
||||
* wallet's. A wormhole address has no key: it is a double Poseidon hash of a
|
||||
* secret the path yields, and funds leave it only through a ZK proof of that
|
||||
* secret. Only the addresses cross into JavaScript; the secrets are derived and
|
||||
* wiped inside WASM.
|
||||
*
|
||||
* The recovery phrase is stretched once per call, so ask for a window of
|
||||
* addresses at once rather than looping over single ones. At most 1000 per call.
|
||||
*/
|
||||
export function wormholeAddresses (mnemonic: string, password: string, account: number, branch: WormholeBranch, start: number, count: number): Uint8Array[] {
|
||||
ready();
|
||||
|
||||
const flat = ext_wormhole_addresses(mnemonic, password, account, branch, start, count);
|
||||
const out: Uint8Array[] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
out.push(flat.slice(i * 32, (i + 1) * 32));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nullifiers for a run of wormhole addresses, one per deposit transfer count.
|
||||
*
|
||||
* A deposit to a wormhole address is spent when its nullifier is in
|
||||
* `Wormhole::UsedNullifiers`. For each address `start..start + addresses` on
|
||||
* `branch` of `account`, this returns the nullifiers for transfer counts
|
||||
* `first..first + count`: `result[a][c]` belongs to address `start + a` and
|
||||
* transfer count `first + c`.
|
||||
*
|
||||
* Check them locally, against a copy of the whole spent set. Never look one up
|
||||
* by key or send it to a service: exits publish their nullifiers, so whoever
|
||||
* sees yours can name your exits. At most 100,000 per call; the recovery phrase
|
||||
* is stretched once per call.
|
||||
*/
|
||||
export function wormholeNullifiers (mnemonic: string, password: string, account: number, branch: WormholeBranch, start: number, addresses: number, first: number, count: number): Uint8Array[][] {
|
||||
ready();
|
||||
|
||||
const flat = ext_wormhole_nullifiers(mnemonic, password, account, branch, start, addresses, BigInt(first), count);
|
||||
const out: Uint8Array[][] = [];
|
||||
|
||||
for (let a = 0; a < addresses; a++) {
|
||||
const row: Uint8Array[] = [];
|
||||
|
||||
for (let c = 0; c < count; c++) {
|
||||
const at = (a * count + c) * 32;
|
||||
|
||||
row.push(flat.slice(at, at + 32));
|
||||
}
|
||||
|
||||
out.push(row);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign under a FIPS 204 context.
|
||||
*
|
||||
* `context` is not optional in spirit: extrinsics on spec >= 148 verify under
|
||||
* `QUANTUS_EXTRINSIC` and earlier ones under the empty context, and the wrong
|
||||
* choice yields a valid signature that the chain rejects with nothing locally
|
||||
* able to tell. Use `contextForSpec` rather than picking one by hand.
|
||||
*
|
||||
* Returns the bare signature. The runtime's wire form is `signature ‖ publicKey`
|
||||
* — see {@link signatureWithPublicKey} — but only the caller knows which it
|
||||
* wants.
|
||||
*/
|
||||
export function sign (message: Uint8Array, { publicKey, secretKey }: Keypair, context: Uint8Array, scheme: Scheme): Uint8Array {
|
||||
ready();
|
||||
|
||||
return ext_mldsa_sign(secretKey, publicKey, message, context, scheme);
|
||||
}
|
||||
|
||||
/** Verify a bare signature under a context. */
|
||||
export function verify (message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array, context: Uint8Array, scheme: Scheme): boolean {
|
||||
ready();
|
||||
|
||||
return ext_mldsa_verify(publicKey, message, signature, context, scheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* `signature ‖ publicKey` — what a signed extrinsic actually carries.
|
||||
*
|
||||
* The runtime encodes this as a fixed-size array with **no compact length
|
||||
* prefix**, preceded by the scheme's enum variant byte. Getting that framing
|
||||
* wrong re-frames every byte after it into something that still decodes.
|
||||
*/
|
||||
export function signatureWithPublicKey (signature: Uint8Array, publicKey: Uint8Array): Uint8Array {
|
||||
const out = new Uint8Array(signature.length + publicKey.length);
|
||||
|
||||
out.set(signature);
|
||||
out.set(publicKey, signature.length);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The crate returns `secretKey ‖ publicKey`, matching `ext_ed_from_seed`. */
|
||||
function split (pair: Uint8Array, scheme: Scheme): Keypair {
|
||||
const { secretKey } = sizes(scheme);
|
||||
|
||||
return {
|
||||
publicKey: pair.subarray(secretKey),
|
||||
secretKey: pair.subarray(0, secretKey)
|
||||
};
|
||||
}
|
||||
226
packages/quantus-crypto/src/generated/quantus_crypto.d.ts
vendored
Normal file
226
packages/quantus-crypto/src/generated/quantus_crypto.d.ts
vendored
Normal file
@@ -0,0 +1,226 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* Derive a keypair from a BIP39 mnemonic at a hardened derivation path.
|
||||
*
|
||||
* Lattice keys have no public derivability, so there is no soft-junction
|
||||
* equivalent and the crate rejects any unhardened path outright. The Quantus
|
||||
* convention is:
|
||||
*
|
||||
* ```text
|
||||
* m/44'/189189'/<account>'/0'/<0 for ML-DSA-87 | 1 for ML-DSA-65>'
|
||||
* ```
|
||||
*
|
||||
* with the account index at the third level and the *scheme* carried in the
|
||||
* trailing index. That is unusual, and it is what `quantus-cli` and the mobile
|
||||
* wallet already use — deriving anything else produces addresses no other
|
||||
* Quantus tool can find.
|
||||
*
|
||||
* The seeding matters as much as the path. This goes mnemonic → 64-byte BIP39
|
||||
* seed → HMAC-SHA512 chain keyed with the literal string `"Dilithium seed"`.
|
||||
* Substrate's own `mnemonicToMiniSecret` is a *different* derivation and is the
|
||||
* default reach in the polkadot-js codebase; using it here would yield a
|
||||
* well-formed key for an account nobody owns.
|
||||
*
|
||||
* * mnemonic: BIP39 phrase, 12/15/18/21/24 words
|
||||
* * password: BIP39 passphrase; empty string for none
|
||||
* * path: hardened derivation path, e.g. `m/44'/189189'/0'/0'/1'`
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is the secret key followed by the public key, as
|
||||
* `ext_mldsa_from_seed` returns.
|
||||
*/
|
||||
export function ext_mldsa_derive(mnemonic: string, password: string, path: string, scheme: number): Uint8Array;
|
||||
|
||||
/**
|
||||
* Generate a keypair from 32 bytes of entropy.
|
||||
*
|
||||
* This is FIPS 204 `ML-DSA.KeyGen_internal` with no Quantus-specific step: the
|
||||
* crate expands the seed as `SHAKE256(seed ‖ k ‖ ℓ)`, so the parameter set is
|
||||
* absorbed into the expansion and the same 32 bytes yield independent keys per
|
||||
* scheme. That is why the dev accounts (`[0u8; 32]`, `[1u8; 32]`, `[2u8; 32]`)
|
||||
* and HD-derived accounts can share this one entry point.
|
||||
*
|
||||
* * seed: UIntArray with 32 elements
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is the secret key followed by the public key, matching the
|
||||
* ordering `ext_ed_from_seed` uses. Split it at the secret length from
|
||||
* `ext_mldsa_sizes`.
|
||||
*/
|
||||
export function ext_mldsa_from_seed(seed: Uint8Array, scheme: number): Uint8Array;
|
||||
|
||||
/**
|
||||
* Whether `scheme` names a parameter set this build supports.
|
||||
*
|
||||
* `dispatch!` falls back to ML-DSA-87 for anything unrecognised, which is the
|
||||
* right default but a poor way to discover a typo. Callers that accept a scheme
|
||||
* from storage or from a user should check here first.
|
||||
*/
|
||||
export function ext_mldsa_is_scheme(scheme: number): boolean;
|
||||
|
||||
/**
|
||||
* Sign a message under a FIPS 204 context.
|
||||
*
|
||||
* Signing is deterministic — no hedging randomness — because that is what the
|
||||
* runtime does (`hedge: None`), and a wallet that hedged would produce a
|
||||
* different signature each time for the same input, which makes the
|
||||
* byte-for-byte agreement tests in quantus/wasm#2 impossible to write.
|
||||
*
|
||||
* `ctx` is domain separation and it is **not** optional in practice: extrinsics
|
||||
* on spec >= 148 are verified under `QUANTUS_EXTRINSIC`, earlier specs under the
|
||||
* empty context, and a signature made under the wrong one is valid, rejected by
|
||||
* the chain, and indistinguishable locally. The caller chooses; this function
|
||||
* does not guess.
|
||||
*
|
||||
* * secret: UIntArray, secret-key length for the scheme
|
||||
* * public: UIntArray, public-key length for the scheme
|
||||
* * message: arbitrary length UIntArray
|
||||
* * ctx: UIntArray, at most 255 elements; empty for no context
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is the signature alone. The runtime's wire format is
|
||||
* `signature ‖ public`; concatenating is the caller's job because only the
|
||||
* caller knows whether it wants the wire form or the bare signature.
|
||||
*/
|
||||
export function ext_mldsa_sign(secret: Uint8Array, _public: Uint8Array, message: Uint8Array, ctx: Uint8Array, scheme: number): Uint8Array;
|
||||
|
||||
/**
|
||||
* Key and signature sizes for a parameter set, as
|
||||
* `[public, secret, signature, signature_with_public]`.
|
||||
*
|
||||
* Exported so that nothing on the JS side has to hardcode 1952/4032/3309/5261 or
|
||||
* 2592/4896/4627/7219. Those numbers are consensus-critical — the runtime reads a
|
||||
* fixed-size array off the wire — and a JS constant that drifted from the crate
|
||||
* would mis-frame every byte after the signature while looking entirely healthy.
|
||||
* Ask the crate instead.
|
||||
*
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is four u32 lengths, little-endian, 16 bytes total.
|
||||
*/
|
||||
export function ext_mldsa_sizes(scheme: number): Uint8Array;
|
||||
|
||||
/**
|
||||
* Verify a signature against a message and public key under a context.
|
||||
*
|
||||
* * public: UIntArray, public-key length for the scheme
|
||||
* * message: arbitrary length UIntArray
|
||||
* * signature: UIntArray, signature length for the scheme
|
||||
* * ctx: UIntArray, at most 255 elements; empty for no context
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*/
|
||||
export function ext_mldsa_verify(_public: Uint8Array, message: Uint8Array, signature: Uint8Array, ctx: Uint8Array, scheme: number): boolean;
|
||||
|
||||
/**
|
||||
* Poseidon2-over-Goldilocks hash of arbitrary bytes.
|
||||
*
|
||||
* This is the account-id derivation. On Substrate an `AccountId32` *is* the
|
||||
* public key; on Quantus it is `hash_bytes(public_key)`, which is why a Quantus
|
||||
* signature has to carry its public key along — the address cannot give it back.
|
||||
*
|
||||
* `qp_poseidon_core::hash_bytes` is `IdentifyAccount for DilithiumSigner` in the
|
||||
* runtime, so this is the same function the chain uses to decide who signed
|
||||
* something, reached through the same crate rather than a port of it.
|
||||
*
|
||||
* * data: arbitrary length UIntArray
|
||||
*
|
||||
* * returned vector is 32 bytes.
|
||||
*/
|
||||
export function ext_poseidon_hash(data: Uint8Array): Uint8Array;
|
||||
|
||||
/**
|
||||
* Derive consecutive wormhole addresses for one account and branch.
|
||||
*
|
||||
* A wormhole address is not a key. The path yields a 32-byte **secret**, and
|
||||
* the address is `poseidon(poseidon(salt ‖ secret))`; funds leave it only
|
||||
* through a ZK proof of knowing that secret. So this returns the addresses and
|
||||
* nothing else. The secrets and the intermediate `first_hash` stay inside this
|
||||
* call and are wiped on drop by the crate's sensitive types.
|
||||
*
|
||||
* Paths are `m/44'/189189189'/<account>'/<change>'/<index>'`, as the mobile
|
||||
* wallet derives them: change `0` is the receive branch, `1` the change branch.
|
||||
*
|
||||
* The BIP39 seed is stretched once for the whole batch. Stretching per address
|
||||
* is PBKDF2 with 2048 rounds each time, and a gap-limit window is dozens of
|
||||
* addresses.
|
||||
*
|
||||
* * mnemonic: BIP39 phrase
|
||||
* * password: BIP39 passphrase; empty string for none
|
||||
* * account, change, start, count: which addresses; count <= WORMHOLE_MAX_BATCH
|
||||
*
|
||||
* * returned vector is `count` 32-byte account ids, concatenated
|
||||
*/
|
||||
export function ext_wormhole_addresses(mnemonic: string, password: string, account: number, change: number, start: number, count: number): Uint8Array;
|
||||
|
||||
/**
|
||||
* Nullifiers for a run of wormhole addresses' deposits, by transfer count.
|
||||
*
|
||||
* A deposit to a wormhole address is spent when its nullifier is in
|
||||
* `Wormhole::UsedNullifiers`. The nullifier is
|
||||
*
|
||||
* ```text
|
||||
* poseidon2(poseidon2(salt("~nullif~") ‖ secret ‖ transfer_count))
|
||||
* ```
|
||||
*
|
||||
* where `transfer_count` is the address's counter when the deposit landed. It
|
||||
* needs the address's secret, which is why this takes the recovery phrase and
|
||||
* why a nullifier should never be sent anywhere to be checked: exits publish
|
||||
* nullifiers, so whoever sees yours can name your exits.
|
||||
*
|
||||
* For addresses `m/44'/189189189'/<account>'/<change>'/<index>'` with index in
|
||||
* `start..start + addresses`, and transfer counts `first..first + count` for
|
||||
* each. The BIP39 seed is stretched once for the whole run.
|
||||
*
|
||||
* Ported onto `qp-poseidon-core` rather than calling `qp-wormhole-circuit`,
|
||||
* which would bring plonky2 into the WASM. The port is pinned to the circuit
|
||||
* crate's own `Nullifier::from_preimage` by the tests.
|
||||
*
|
||||
* * returned vector is `addresses * count` 32-byte nullifiers: address by
|
||||
* address, and by transfer count within each
|
||||
*/
|
||||
export function ext_wormhole_nullifiers(mnemonic: string, password: string, account: number, change: number, start: number, addresses: number, first: bigint, count: number): Uint8Array;
|
||||
|
||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||
|
||||
export interface InitOutput {
|
||||
readonly memory: WebAssembly.Memory;
|
||||
readonly ext_mldsa_derive: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number, number, number];
|
||||
readonly ext_mldsa_from_seed: (a: number, b: number, c: number) => [number, number, number, number];
|
||||
readonly ext_mldsa_is_scheme: (a: number) => number;
|
||||
readonly ext_mldsa_sign: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => [number, number, number, number];
|
||||
readonly ext_mldsa_sizes: (a: number) => [number, number];
|
||||
readonly ext_mldsa_verify: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number) => number;
|
||||
readonly ext_poseidon_hash: (a: number, b: number) => [number, number];
|
||||
readonly ext_wormhole_addresses: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number, number, number];
|
||||
readonly ext_wormhole_nullifiers: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: bigint, j: number) => [number, number, number, number];
|
||||
readonly __wbindgen_externrefs: WebAssembly.Table;
|
||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||
readonly __externref_table_dealloc: (a: number) => void;
|
||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||
readonly __wbindgen_start: () => void;
|
||||
}
|
||||
|
||||
export type SyncInitInput = BufferSource | WebAssembly.Module;
|
||||
|
||||
/**
|
||||
* Instantiates the given `module`, which can either be bytes or
|
||||
* a precompiled `WebAssembly.Module`.
|
||||
*
|
||||
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {InitOutput}
|
||||
*/
|
||||
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
|
||||
|
||||
/**
|
||||
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
|
||||
* for everything else, calls `WebAssembly.instantiate` directly.
|
||||
*
|
||||
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
|
||||
*
|
||||
* @returns {Promise<InitOutput>}
|
||||
*/
|
||||
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
|
||||
503
packages/quantus-crypto/src/generated/quantus_crypto.js
Normal file
503
packages/quantus-crypto/src/generated/quantus_crypto.js
Normal file
@@ -0,0 +1,503 @@
|
||||
/* @ts-self-types="./quantus_crypto.d.ts" */
|
||||
|
||||
/**
|
||||
* Derive a keypair from a BIP39 mnemonic at a hardened derivation path.
|
||||
*
|
||||
* Lattice keys have no public derivability, so there is no soft-junction
|
||||
* equivalent and the crate rejects any unhardened path outright. The Quantus
|
||||
* convention is:
|
||||
*
|
||||
* ```text
|
||||
* m/44'/189189'/<account>'/0'/<0 for ML-DSA-87 | 1 for ML-DSA-65>'
|
||||
* ```
|
||||
*
|
||||
* with the account index at the third level and the *scheme* carried in the
|
||||
* trailing index. That is unusual, and it is what `quantus-cli` and the mobile
|
||||
* wallet already use — deriving anything else produces addresses no other
|
||||
* Quantus tool can find.
|
||||
*
|
||||
* The seeding matters as much as the path. This goes mnemonic → 64-byte BIP39
|
||||
* seed → HMAC-SHA512 chain keyed with the literal string `"Dilithium seed"`.
|
||||
* Substrate's own `mnemonicToMiniSecret` is a *different* derivation and is the
|
||||
* default reach in the polkadot-js codebase; using it here would yield a
|
||||
* well-formed key for an account nobody owns.
|
||||
*
|
||||
* * mnemonic: BIP39 phrase, 12/15/18/21/24 words
|
||||
* * password: BIP39 passphrase; empty string for none
|
||||
* * path: hardened derivation path, e.g. `m/44'/189189'/0'/0'/1'`
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is the secret key followed by the public key, as
|
||||
* `ext_mldsa_from_seed` returns.
|
||||
* @param {string} mnemonic
|
||||
* @param {string} password
|
||||
* @param {string} path
|
||||
* @param {number} scheme
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function ext_mldsa_derive(mnemonic, password, path, scheme) {
|
||||
const ptr0 = passStringToWasm0(mnemonic, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ptr2 = passStringToWasm0(path, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len2 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.ext_mldsa_derive(ptr0, len0, ptr1, len1, ptr2, len2, scheme);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v4 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a keypair from 32 bytes of entropy.
|
||||
*
|
||||
* This is FIPS 204 `ML-DSA.KeyGen_internal` with no Quantus-specific step: the
|
||||
* crate expands the seed as `SHAKE256(seed ‖ k ‖ ℓ)`, so the parameter set is
|
||||
* absorbed into the expansion and the same 32 bytes yield independent keys per
|
||||
* scheme. That is why the dev accounts (`[0u8; 32]`, `[1u8; 32]`, `[2u8; 32]`)
|
||||
* and HD-derived accounts can share this one entry point.
|
||||
*
|
||||
* * seed: UIntArray with 32 elements
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is the secret key followed by the public key, matching the
|
||||
* ordering `ext_ed_from_seed` uses. Split it at the secret length from
|
||||
* `ext_mldsa_sizes`.
|
||||
* @param {Uint8Array} seed
|
||||
* @param {number} scheme
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function ext_mldsa_from_seed(seed, scheme) {
|
||||
const ptr0 = passArray8ToWasm0(seed, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.ext_mldsa_from_seed(ptr0, len0, scheme);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether `scheme` names a parameter set this build supports.
|
||||
*
|
||||
* `dispatch!` falls back to ML-DSA-87 for anything unrecognised, which is the
|
||||
* right default but a poor way to discover a typo. Callers that accept a scheme
|
||||
* from storage or from a user should check here first.
|
||||
* @param {number} scheme
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function ext_mldsa_is_scheme(scheme) {
|
||||
const ret = wasm.ext_mldsa_is_scheme(scheme);
|
||||
return ret !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a message under a FIPS 204 context.
|
||||
*
|
||||
* Signing is deterministic — no hedging randomness — because that is what the
|
||||
* runtime does (`hedge: None`), and a wallet that hedged would produce a
|
||||
* different signature each time for the same input, which makes the
|
||||
* byte-for-byte agreement tests in quantus/wasm#2 impossible to write.
|
||||
*
|
||||
* `ctx` is domain separation and it is **not** optional in practice: extrinsics
|
||||
* on spec >= 148 are verified under `QUANTUS_EXTRINSIC`, earlier specs under the
|
||||
* empty context, and a signature made under the wrong one is valid, rejected by
|
||||
* the chain, and indistinguishable locally. The caller chooses; this function
|
||||
* does not guess.
|
||||
*
|
||||
* * secret: UIntArray, secret-key length for the scheme
|
||||
* * public: UIntArray, public-key length for the scheme
|
||||
* * message: arbitrary length UIntArray
|
||||
* * ctx: UIntArray, at most 255 elements; empty for no context
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is the signature alone. The runtime's wire format is
|
||||
* `signature ‖ public`; concatenating is the caller's job because only the
|
||||
* caller knows whether it wants the wire form or the bare signature.
|
||||
* @param {Uint8Array} secret
|
||||
* @param {Uint8Array} _public
|
||||
* @param {Uint8Array} message
|
||||
* @param {Uint8Array} ctx
|
||||
* @param {number} scheme
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function ext_mldsa_sign(secret, _public, message, ctx, scheme) {
|
||||
const ptr0 = passArray8ToWasm0(secret, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passArray8ToWasm0(_public, wasm.__wbindgen_malloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ptr2 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
|
||||
const len2 = WASM_VECTOR_LEN;
|
||||
const ptr3 = passArray8ToWasm0(ctx, wasm.__wbindgen_malloc);
|
||||
const len3 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.ext_mldsa_sign(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, scheme);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v5 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key and signature sizes for a parameter set, as
|
||||
* `[public, secret, signature, signature_with_public]`.
|
||||
*
|
||||
* Exported so that nothing on the JS side has to hardcode 1952/4032/3309/5261 or
|
||||
* 2592/4896/4627/7219. Those numbers are consensus-critical — the runtime reads a
|
||||
* fixed-size array off the wire — and a JS constant that drifted from the crate
|
||||
* would mis-frame every byte after the signature while looking entirely healthy.
|
||||
* Ask the crate instead.
|
||||
*
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
*
|
||||
* * returned vector is four u32 lengths, little-endian, 16 bytes total.
|
||||
* @param {number} scheme
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function ext_mldsa_sizes(scheme) {
|
||||
const ret = wasm.ext_mldsa_sizes(scheme);
|
||||
var v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a signature against a message and public key under a context.
|
||||
*
|
||||
* * public: UIntArray, public-key length for the scheme
|
||||
* * message: arbitrary length UIntArray
|
||||
* * signature: UIntArray, signature length for the scheme
|
||||
* * ctx: UIntArray, at most 255 elements; empty for no context
|
||||
* * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
* @param {Uint8Array} _public
|
||||
* @param {Uint8Array} message
|
||||
* @param {Uint8Array} signature
|
||||
* @param {Uint8Array} ctx
|
||||
* @param {number} scheme
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function ext_mldsa_verify(_public, message, signature, ctx, scheme) {
|
||||
const ptr0 = passArray8ToWasm0(_public, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ptr2 = passArray8ToWasm0(signature, wasm.__wbindgen_malloc);
|
||||
const len2 = WASM_VECTOR_LEN;
|
||||
const ptr3 = passArray8ToWasm0(ctx, wasm.__wbindgen_malloc);
|
||||
const len3 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.ext_mldsa_verify(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, scheme);
|
||||
return ret !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Poseidon2-over-Goldilocks hash of arbitrary bytes.
|
||||
*
|
||||
* This is the account-id derivation. On Substrate an `AccountId32` *is* the
|
||||
* public key; on Quantus it is `hash_bytes(public_key)`, which is why a Quantus
|
||||
* signature has to carry its public key along — the address cannot give it back.
|
||||
*
|
||||
* `qp_poseidon_core::hash_bytes` is `IdentifyAccount for DilithiumSigner` in the
|
||||
* runtime, so this is the same function the chain uses to decide who signed
|
||||
* something, reached through the same crate rather than a port of it.
|
||||
*
|
||||
* * data: arbitrary length UIntArray
|
||||
*
|
||||
* * returned vector is 32 bytes.
|
||||
* @param {Uint8Array} data
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function ext_poseidon_hash(data) {
|
||||
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.ext_poseidon_hash(ptr0, len0);
|
||||
var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive consecutive wormhole addresses for one account and branch.
|
||||
*
|
||||
* A wormhole address is not a key. The path yields a 32-byte **secret**, and
|
||||
* the address is `poseidon(poseidon(salt ‖ secret))`; funds leave it only
|
||||
* through a ZK proof of knowing that secret. So this returns the addresses and
|
||||
* nothing else. The secrets and the intermediate `first_hash` stay inside this
|
||||
* call and are wiped on drop by the crate's sensitive types.
|
||||
*
|
||||
* Paths are `m/44'/189189189'/<account>'/<change>'/<index>'`, as the mobile
|
||||
* wallet derives them: change `0` is the receive branch, `1` the change branch.
|
||||
*
|
||||
* The BIP39 seed is stretched once for the whole batch. Stretching per address
|
||||
* is PBKDF2 with 2048 rounds each time, and a gap-limit window is dozens of
|
||||
* addresses.
|
||||
*
|
||||
* * mnemonic: BIP39 phrase
|
||||
* * password: BIP39 passphrase; empty string for none
|
||||
* * account, change, start, count: which addresses; count <= WORMHOLE_MAX_BATCH
|
||||
*
|
||||
* * returned vector is `count` 32-byte account ids, concatenated
|
||||
* @param {string} mnemonic
|
||||
* @param {string} password
|
||||
* @param {number} account
|
||||
* @param {number} change
|
||||
* @param {number} start
|
||||
* @param {number} count
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function ext_wormhole_addresses(mnemonic, password, account, change, start, count) {
|
||||
const ptr0 = passStringToWasm0(mnemonic, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.ext_wormhole_addresses(ptr0, len0, ptr1, len1, account, change, start, count);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v3 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nullifiers for a run of wormhole addresses' deposits, by transfer count.
|
||||
*
|
||||
* A deposit to a wormhole address is spent when its nullifier is in
|
||||
* `Wormhole::UsedNullifiers`. The nullifier is
|
||||
*
|
||||
* ```text
|
||||
* poseidon2(poseidon2(salt("~nullif~") ‖ secret ‖ transfer_count))
|
||||
* ```
|
||||
*
|
||||
* where `transfer_count` is the address's counter when the deposit landed. It
|
||||
* needs the address's secret, which is why this takes the recovery phrase and
|
||||
* why a nullifier should never be sent anywhere to be checked: exits publish
|
||||
* nullifiers, so whoever sees yours can name your exits.
|
||||
*
|
||||
* For addresses `m/44'/189189189'/<account>'/<change>'/<index>'` with index in
|
||||
* `start..start + addresses`, and transfer counts `first..first + count` for
|
||||
* each. The BIP39 seed is stretched once for the whole run.
|
||||
*
|
||||
* Ported onto `qp-poseidon-core` rather than calling `qp-wormhole-circuit`,
|
||||
* which would bring plonky2 into the WASM. The port is pinned to the circuit
|
||||
* crate's own `Nullifier::from_preimage` by the tests.
|
||||
*
|
||||
* * returned vector is `addresses * count` 32-byte nullifiers: address by
|
||||
* address, and by transfer count within each
|
||||
* @param {string} mnemonic
|
||||
* @param {string} password
|
||||
* @param {number} account
|
||||
* @param {number} change
|
||||
* @param {number} start
|
||||
* @param {number} addresses
|
||||
* @param {bigint} first
|
||||
* @param {number} count
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
export function ext_wormhole_nullifiers(mnemonic, password, account, change, start, addresses, first, count) {
|
||||
const ptr0 = passStringToWasm0(mnemonic, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len0 = WASM_VECTOR_LEN;
|
||||
const ptr1 = passStringToWasm0(password, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
const len1 = WASM_VECTOR_LEN;
|
||||
const ret = wasm.ext_wormhole_nullifiers(ptr0, len0, ptr1, len1, account, change, start, addresses, first, count);
|
||||
if (ret[3]) {
|
||||
throw takeFromExternrefTable0(ret[2]);
|
||||
}
|
||||
var v3 = getArrayU8FromWasm0(ret[0], ret[1]).slice();
|
||||
wasm.__wbindgen_free(ret[0], ret[1] * 1, 1);
|
||||
return v3;
|
||||
}
|
||||
function __wbg_get_imports() {
|
||||
const import0 = {
|
||||
__proto__: null,
|
||||
__wbg_Error_67e7344beaa85059: function(arg0, arg1) {
|
||||
const ret = Error(getStringFromWasm0(arg0, arg1));
|
||||
return ret;
|
||||
},
|
||||
__wbindgen_init_externref_table: function() {
|
||||
const table = wasm.__wbindgen_externrefs;
|
||||
const offset = table.grow(4);
|
||||
table.set(0, undefined);
|
||||
table.set(offset + 0, undefined);
|
||||
table.set(offset + 1, null);
|
||||
table.set(offset + 2, true);
|
||||
table.set(offset + 3, false);
|
||||
},
|
||||
};
|
||||
return {
|
||||
__proto__: null,
|
||||
"./quantus_crypto_bg.js": import0,
|
||||
};
|
||||
}
|
||||
|
||||
function getArrayU8FromWasm0(ptr, len) {
|
||||
ptr = ptr >>> 0;
|
||||
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||
}
|
||||
|
||||
function getStringFromWasm0(ptr, len) {
|
||||
return decodeText(ptr >>> 0, len);
|
||||
}
|
||||
|
||||
let cachedUint8ArrayMemory0 = null;
|
||||
function getUint8ArrayMemory0() {
|
||||
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
|
||||
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedUint8ArrayMemory0;
|
||||
}
|
||||
|
||||
function passArray8ToWasm0(arg, malloc) {
|
||||
const ptr = malloc(arg.length * 1, 1) >>> 0;
|
||||
getUint8ArrayMemory0().set(arg, ptr / 1);
|
||||
WASM_VECTOR_LEN = arg.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function passStringToWasm0(arg, malloc, realloc) {
|
||||
if (realloc === undefined) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
const ptr = malloc(buf.length, 1) >>> 0;
|
||||
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
|
||||
WASM_VECTOR_LEN = buf.length;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
let len = arg.length;
|
||||
let ptr = malloc(len, 1) >>> 0;
|
||||
|
||||
const mem = getUint8ArrayMemory0();
|
||||
|
||||
let offset = 0;
|
||||
|
||||
for (; offset < len; offset++) {
|
||||
const code = arg.charCodeAt(offset);
|
||||
if (code > 0x7F) break;
|
||||
mem[ptr + offset] = code;
|
||||
}
|
||||
if (offset !== len) {
|
||||
if (offset !== 0) {
|
||||
arg = arg.slice(offset);
|
||||
}
|
||||
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
|
||||
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
|
||||
const ret = cachedTextEncoder.encodeInto(arg, view);
|
||||
|
||||
offset += ret.written;
|
||||
ptr = realloc(ptr, len, offset, 1) >>> 0;
|
||||
}
|
||||
|
||||
WASM_VECTOR_LEN = offset;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
function takeFromExternrefTable0(idx) {
|
||||
const value = wasm.__wbindgen_externrefs.get(idx);
|
||||
wasm.__externref_table_dealloc(idx);
|
||||
return value;
|
||||
}
|
||||
|
||||
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
cachedTextDecoder.decode();
|
||||
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
||||
let numBytesDecoded = 0;
|
||||
function decodeText(ptr, len) {
|
||||
numBytesDecoded += len;
|
||||
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
|
||||
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||
cachedTextDecoder.decode();
|
||||
numBytesDecoded = len;
|
||||
}
|
||||
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
const cachedTextEncoder = new TextEncoder();
|
||||
|
||||
if (!('encodeInto' in cachedTextEncoder)) {
|
||||
cachedTextEncoder.encodeInto = function (arg, view) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
view.set(buf);
|
||||
return {
|
||||
read: arg.length,
|
||||
written: buf.length
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
let WASM_VECTOR_LEN = 0;
|
||||
|
||||
let wasmModule, wasmInstance, wasm;
|
||||
function __wbg_finalize_init(instance, module) {
|
||||
wasmInstance = instance;
|
||||
wasm = instance.exports;
|
||||
wasmModule = module;
|
||||
cachedUint8ArrayMemory0 = null;
|
||||
wasm.__wbindgen_start();
|
||||
return wasm;
|
||||
}
|
||||
|
||||
async function __wbg_load(module, imports) {
|
||||
if (typeof Response === 'function' && module instanceof Response) {
|
||||
if (!module.ok) {
|
||||
throw new Error(`failed to fetch Wasm: ${module.status} ${module.statusText} fetching '${module.url}'`);
|
||||
}
|
||||
|
||||
if (typeof WebAssembly.instantiateStreaming === 'function') {
|
||||
try {
|
||||
return await WebAssembly.instantiateStreaming(module, imports);
|
||||
} catch (e) {
|
||||
const validResponse = expectedResponseType(module.type);
|
||||
|
||||
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
|
||||
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
|
||||
|
||||
} else { throw e; }
|
||||
}
|
||||
}
|
||||
|
||||
const bytes = await module.arrayBuffer();
|
||||
return await WebAssembly.instantiate(bytes, imports);
|
||||
} else {
|
||||
const instance = await WebAssembly.instantiate(module, imports);
|
||||
|
||||
if (instance instanceof WebAssembly.Instance) {
|
||||
return { instance, module };
|
||||
} else {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
function expectedResponseType(type) {
|
||||
switch (type) {
|
||||
case 'basic': case 'cors': case 'default': return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function initSync(module) {
|
||||
if (wasm !== undefined) return wasm;
|
||||
|
||||
|
||||
if (module !== undefined) {
|
||||
if (Object.getPrototypeOf(module) === Object.prototype) {
|
||||
({module} = module)
|
||||
} else {
|
||||
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
|
||||
}
|
||||
}
|
||||
|
||||
const imports = __wbg_get_imports();
|
||||
if (!(module instanceof WebAssembly.Module)) {
|
||||
module = new WebAssembly.Module(module);
|
||||
}
|
||||
const instance = new WebAssembly.Instance(module, imports);
|
||||
return __wbg_finalize_init(instance, module);
|
||||
}
|
||||
|
||||
export { initSync };
|
||||
7
packages/quantus-crypto/src/index.ts
Normal file
7
packages/quantus-crypto/src/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export { accountFromPublicKey, isScheme, keypairFromMnemonic, keypairFromSeed, sign, signatureWithPublicKey, sizes, verify, WormholeBranch, wormholeAddresses, wormholeNullifiers } from './crypto.js';
|
||||
export type { Keypair, Sizes } from './crypto.js';
|
||||
export { initWasm, isReady } from './init.js';
|
||||
export { contextForSpec, EXTRINSIC_CONTEXT, EXTRINSIC_MIN_SPEC, Scheme, SCHEME_NAME } from './scheme.js';
|
||||
61
packages/quantus-crypto/src/init.ts
Normal file
61
packages/quantus-crypto/src/init.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { unzlibSync } from 'fflate';
|
||||
|
||||
import { base64Decode } from './base64.js';
|
||||
|
||||
import { bytes, lenOut } from './bytes.js';
|
||||
import { initSync } from './generated/quantus_crypto.js';
|
||||
|
||||
/**
|
||||
* Instantiate the WASM, synchronously, from bytes compiled into this file.
|
||||
*
|
||||
* Three constraints shape this, and all three rule out the obvious approach:
|
||||
*
|
||||
* - the background context is an **MV3 service worker**, so there is no DOM, no
|
||||
* reliable `fetch` of extension-relative URLs at arbitrary times, and the
|
||||
* worker can be killed and cold-started between any two messages
|
||||
* - the extension CSP is `script-src 'self' 'wasm-unsafe-eval'`, which permits
|
||||
* compiling WASM but not fetching it from anywhere interesting
|
||||
* - callers are synchronous — `pair.sign()` in the keyring has no `await` to give
|
||||
*
|
||||
* So the WASM is zlib-compressed, base64'd into `bytes.js` at build time, and
|
||||
* instantiated here with wasm-bindgen's `initSync`. Nothing is fetched, and the
|
||||
* whole module is ready before the first call returns.
|
||||
*
|
||||
* Deliberately *not* using `@polkadot/wasm-bridge`: its `Bridge` implements
|
||||
* wasm-bindgen 0.2.79's JS-heap ABI, and this crate is built with 0.2.128, which
|
||||
* uses externref tables. See quantus/wasm#1.
|
||||
*/
|
||||
|
||||
let initialised = false;
|
||||
let initError: string | null = null;
|
||||
|
||||
/**
|
||||
* Ensure the WASM is instantiated. Idempotent and cheap after the first call.
|
||||
*
|
||||
* Returns `null` on success, or the failure reason. It does not throw: a caller
|
||||
* deciding whether to offer a Quantus account at all wants to ask, and an
|
||||
* exception thrown from module scope in a service worker is hard to attribute.
|
||||
*/
|
||||
export function initWasm (): string | null {
|
||||
if (initialised) {
|
||||
return initError;
|
||||
}
|
||||
|
||||
initialised = true;
|
||||
|
||||
try {
|
||||
initSync({ module: unzlibSync(base64Decode(bytes, new Uint8Array(lenOut))) });
|
||||
} catch (error) {
|
||||
initError = (error as Error).message;
|
||||
}
|
||||
|
||||
return initError;
|
||||
}
|
||||
|
||||
/** Whether the WASM is available. Callers that can fall back should ask first. */
|
||||
export function isReady (): boolean {
|
||||
return initWasm() === null;
|
||||
}
|
||||
27
packages/quantus-crypto/src/lib.rs
Normal file
27
packages/quantus-crypto/src/lib.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Quantus post-quantum crypto for the browser.
|
||||
//!
|
||||
//! Every function here delegates to the crates the Quantus runtime itself uses
|
||||
//! (`qp-rusty-crystals-dilithium`, `qp-poseidon-core`, `qp-rusty-crystals-hdwallet`)
|
||||
//! rather than reimplementing anything. That is the whole point: a browser wallet
|
||||
//! that disagreed with the chain about a key or a signature would produce
|
||||
//! perfectly well-formed output that the chain rejects, and nothing on this side
|
||||
//! could tell.
|
||||
|
||||
#[path = "rs/hdwallet.rs"]
|
||||
pub mod hdwallet;
|
||||
|
||||
#[path = "rs/mldsa.rs"]
|
||||
pub mod mldsa;
|
||||
|
||||
#[path = "rs/poseidon.rs"]
|
||||
pub mod poseidon;
|
||||
|
||||
#[path = "rs/scheme.rs"]
|
||||
pub mod scheme;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "rs/tests.rs"]
|
||||
mod tests;
|
||||
231
packages/quantus-crypto/src/rs/hdwallet.rs
Normal file
231
packages/quantus-crypto/src/rs/hdwallet.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::scheme::dispatch;
|
||||
|
||||
/// Derive a keypair from a BIP39 mnemonic at a hardened derivation path.
|
||||
///
|
||||
/// Lattice keys have no public derivability, so there is no soft-junction
|
||||
/// equivalent and the crate rejects any unhardened path outright. The Quantus
|
||||
/// convention is:
|
||||
///
|
||||
/// ```text
|
||||
/// m/44'/189189'/<account>'/0'/<0 for ML-DSA-87 | 1 for ML-DSA-65>'
|
||||
/// ```
|
||||
///
|
||||
/// with the account index at the third level and the *scheme* carried in the
|
||||
/// trailing index. That is unusual, and it is what `quantus-cli` and the mobile
|
||||
/// wallet already use — deriving anything else produces addresses no other
|
||||
/// Quantus tool can find.
|
||||
///
|
||||
/// The seeding matters as much as the path. This goes mnemonic → 64-byte BIP39
|
||||
/// seed → HMAC-SHA512 chain keyed with the literal string `"Dilithium seed"`.
|
||||
/// Substrate's own `mnemonicToMiniSecret` is a *different* derivation and is the
|
||||
/// default reach in the polkadot-js codebase; using it here would yield a
|
||||
/// well-formed key for an account nobody owns.
|
||||
///
|
||||
/// * mnemonic: BIP39 phrase, 12/15/18/21/24 words
|
||||
/// * password: BIP39 passphrase; empty string for none
|
||||
/// * path: hardened derivation path, e.g. `m/44'/189189'/0'/0'/1'`
|
||||
/// * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
///
|
||||
/// * returned vector is the secret key followed by the public key, as
|
||||
/// `ext_mldsa_from_seed` returns.
|
||||
#[wasm_bindgen]
|
||||
pub fn ext_mldsa_derive(mnemonic: &str, password: &str, path: &str, scheme: u32) -> Result<Vec<u8>, JsError> {
|
||||
mldsa_derive(mnemonic, password, path, scheme).map_err(|e| JsError::new(&e))
|
||||
}
|
||||
|
||||
/// The body of [`ext_mldsa_derive`]. See [`crate::mldsa::mldsa_from_seed`] for why
|
||||
/// this is split from its binding.
|
||||
pub fn mldsa_derive(mnemonic: &str, password: &str, path: &str, scheme: u32) -> Result<Vec<u8>, String> {
|
||||
// An empty passphrase and no passphrase are the same thing in BIP39, but the
|
||||
// crate distinguishes `None` from `Some("")` in its signature, so normalise
|
||||
// here rather than leaving each caller to pick one.
|
||||
let password = if password.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(password)
|
||||
};
|
||||
|
||||
dispatch!(scheme, _dsa, hd, {
|
||||
let pair = hd::derive_key_from_mnemonic(mnemonic, password, path)
|
||||
.map_err(alloc_error)?;
|
||||
let mut out = pair.secret().to_bytes().to_vec();
|
||||
|
||||
out.extend_from_slice(&pair.public().to_bytes());
|
||||
|
||||
Ok(out)
|
||||
})
|
||||
}
|
||||
|
||||
/// Render a derivation failure as a string.
|
||||
///
|
||||
/// Kept separate so the error text stays whatever the crate said — a bad
|
||||
/// mnemonic, an unhardened path and a path that is too deep are different
|
||||
/// mistakes and a user can only fix the one they made.
|
||||
fn alloc_error(e: qp_rusty_crystals_hdwallet::HDLatticeError) -> String {
|
||||
format!("{e}")
|
||||
}
|
||||
|
||||
/// The largest number of wormhole addresses one call derives. Each is a Poseidon
|
||||
/// hash over an HMAC-SHA512 chain; the bound keeps a caller from asking for a
|
||||
/// million of them and locking the page.
|
||||
pub const WORMHOLE_MAX_BATCH: u32 = 1000;
|
||||
|
||||
/// Derive consecutive wormhole addresses for one account and branch.
|
||||
///
|
||||
/// A wormhole address is not a key. The path yields a 32-byte **secret**, and
|
||||
/// the address is `poseidon(poseidon(salt ‖ secret))`; funds leave it only
|
||||
/// through a ZK proof of knowing that secret. So this returns the addresses and
|
||||
/// nothing else. The secrets and the intermediate `first_hash` stay inside this
|
||||
/// call and are wiped on drop by the crate's sensitive types.
|
||||
///
|
||||
/// Paths are `m/44'/189189189'/<account>'/<change>'/<index>'`, as the mobile
|
||||
/// wallet derives them: change `0` is the receive branch, `1` the change branch.
|
||||
///
|
||||
/// The BIP39 seed is stretched once for the whole batch. Stretching per address
|
||||
/// is PBKDF2 with 2048 rounds each time, and a gap-limit window is dozens of
|
||||
/// addresses.
|
||||
///
|
||||
/// * mnemonic: BIP39 phrase
|
||||
/// * password: BIP39 passphrase; empty string for none
|
||||
/// * account, change, start, count: which addresses; count <= WORMHOLE_MAX_BATCH
|
||||
///
|
||||
/// * returned vector is `count` 32-byte account ids, concatenated
|
||||
#[wasm_bindgen]
|
||||
pub fn ext_wormhole_addresses(mnemonic: &str, password: &str, account: u32, change: u32, start: u32, count: u32) -> Result<Vec<u8>, JsError> {
|
||||
wormhole_addresses(mnemonic, password, account, change, start, count).map_err(|e| JsError::new(&e))
|
||||
}
|
||||
|
||||
/// The body of [`ext_wormhole_addresses`], split from its binding for the same
|
||||
/// reason as [`mldsa_derive`].
|
||||
pub fn wormhole_addresses(mnemonic: &str, password: &str, account: u32, change: u32, start: u32, count: u32) -> Result<Vec<u8>, String> {
|
||||
use qp_rusty_crystals_hdwallet::{generate_wormhole_from_seed, mnemonic_to_seed, SensitiveBytes64};
|
||||
|
||||
if count > WORMHOLE_MAX_BATCH {
|
||||
return Err(format!("At most {WORMHOLE_MAX_BATCH} wormhole addresses per call, asked for {count}"));
|
||||
}
|
||||
|
||||
if start.checked_add(count).is_none_or(|end| end > 0x8000_0000) {
|
||||
return Err(format!("Wormhole address indices must stay below 2^31, asked for {start} + {count}"));
|
||||
}
|
||||
|
||||
let password = if password.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(password)
|
||||
};
|
||||
let mut seed = SensitiveBytes64::zeroed();
|
||||
|
||||
mnemonic_to_seed(mnemonic.to_string(), password, &mut seed).map_err(alloc_error)?;
|
||||
|
||||
let mut out = Vec::with_capacity(count as usize * 32);
|
||||
|
||||
for index in start..start + count {
|
||||
let path = format!("m/44'/189189189'/{account}'/{change}'/{index}'");
|
||||
let pair = generate_wormhole_from_seed(&seed, &path).map_err(alloc_error)?;
|
||||
|
||||
out.extend_from_slice(pair.address());
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Salt the chain's nullifier derivation starts from: `NULLIFIER_SALT` in
|
||||
/// `qp-wormhole-circuit`.
|
||||
const NULLIFIER_SALT: &str = "~nullif~";
|
||||
|
||||
/// The largest number of nullifiers one call computes.
|
||||
pub const NULLIFIER_MAX_BATCH: u32 = 100_000;
|
||||
|
||||
/// Nullifiers for a run of wormhole addresses' deposits, by transfer count.
|
||||
///
|
||||
/// A deposit to a wormhole address is spent when its nullifier is in
|
||||
/// `Wormhole::UsedNullifiers`. The nullifier is
|
||||
///
|
||||
/// ```text
|
||||
/// poseidon2(poseidon2(salt("~nullif~") ‖ secret ‖ transfer_count))
|
||||
/// ```
|
||||
///
|
||||
/// where `transfer_count` is the address's counter when the deposit landed. It
|
||||
/// needs the address's secret, which is why this takes the recovery phrase and
|
||||
/// why a nullifier should never be sent anywhere to be checked: exits publish
|
||||
/// nullifiers, so whoever sees yours can name your exits.
|
||||
///
|
||||
/// For addresses `m/44'/189189189'/<account>'/<change>'/<index>'` with index in
|
||||
/// `start..start + addresses`, and transfer counts `first..first + count` for
|
||||
/// each. The BIP39 seed is stretched once for the whole run.
|
||||
///
|
||||
/// Ported onto `qp-poseidon-core` rather than calling `qp-wormhole-circuit`,
|
||||
/// which would bring plonky2 into the WASM. The port is pinned to the circuit
|
||||
/// crate's own `Nullifier::from_preimage` by the tests.
|
||||
///
|
||||
/// * returned vector is `addresses * count` 32-byte nullifiers: address by
|
||||
/// address, and by transfer count within each
|
||||
#[wasm_bindgen]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn ext_wormhole_nullifiers(mnemonic: &str, password: &str, account: u32, change: u32, start: u32, addresses: u32, first: u64, count: u32) -> Result<Vec<u8>, JsError> {
|
||||
wormhole_nullifiers(mnemonic, password, account, change, start, addresses, first, count).map_err(|e| JsError::new(&e))
|
||||
}
|
||||
|
||||
/// The body of [`ext_wormhole_nullifiers`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn wormhole_nullifiers(mnemonic: &str, password: &str, account: u32, change: u32, start: u32, addresses: u32, first: u64, count: u32) -> Result<Vec<u8>, String> {
|
||||
use qp_rusty_crystals_hdwallet::{generate_wormhole_from_seed, mnemonic_to_seed, SensitiveBytes64};
|
||||
|
||||
let total = addresses as u64 * count as u64;
|
||||
|
||||
if total > NULLIFIER_MAX_BATCH as u64 {
|
||||
return Err(format!("At most {NULLIFIER_MAX_BATCH} nullifiers per call, asked for {total}"));
|
||||
}
|
||||
|
||||
if start.checked_add(addresses).is_none_or(|end| end > 0x8000_0000) {
|
||||
return Err(format!("Wormhole address indices must stay below 2^31, asked for {start} + {addresses}"));
|
||||
}
|
||||
|
||||
first.checked_add(count as u64).ok_or("Transfer counts overflow")?;
|
||||
|
||||
let password = if password.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(password)
|
||||
};
|
||||
let mut seed = SensitiveBytes64::zeroed();
|
||||
|
||||
mnemonic_to_seed(mnemonic.to_string(), password, &mut seed).map_err(alloc_error)?;
|
||||
|
||||
let mut out = Vec::with_capacity(total as usize * 32);
|
||||
|
||||
for index in start..start + addresses {
|
||||
let path = format!("m/44'/189189189'/{account}'/{change}'/{index}'");
|
||||
let pair = generate_wormhole_from_seed(&seed, &path).map_err(alloc_error)?;
|
||||
|
||||
for transfer_count in first..first + count as u64 {
|
||||
out.extend_from_slice(&nullifier(pair.secret().as_bytes(), transfer_count));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// One nullifier from a wormhole secret and a transfer count.
|
||||
pub fn nullifier(secret: &[u8; 32], transfer_count: u64) -> [u8; 32] {
|
||||
use qp_poseidon_core::{
|
||||
hash_twice,
|
||||
serialization::{bytes_to_digest_lossy, string_to_felts, u64_to_felts},
|
||||
};
|
||||
|
||||
let salt = string_to_felts(NULLIFIER_SALT);
|
||||
let secret_felts = bytes_to_digest_lossy(secret);
|
||||
let count_felts = u64_to_felts(transfer_count);
|
||||
let mut preimage = Vec::with_capacity(salt.len() + secret_felts.len() + count_felts.len());
|
||||
|
||||
preimage.extend_from_slice(&salt);
|
||||
preimage.extend_from_slice(&secret_felts);
|
||||
preimage.extend_from_slice(&count_felts);
|
||||
|
||||
hash_twice(&preimage)
|
||||
}
|
||||
164
packages/quantus-crypto/src/rs/mldsa.rs
Normal file
164
packages/quantus-crypto/src/rs/mldsa.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::scheme::dispatch;
|
||||
|
||||
/// Key and signature sizes for a parameter set, as
|
||||
/// `[public, secret, signature, signature_with_public]`.
|
||||
///
|
||||
/// Exported so that nothing on the JS side has to hardcode 1952/4032/3309/5261 or
|
||||
/// 2592/4896/4627/7219. Those numbers are consensus-critical — the runtime reads a
|
||||
/// fixed-size array off the wire — and a JS constant that drifted from the crate
|
||||
/// would mis-frame every byte after the signature while looking entirely healthy.
|
||||
/// Ask the crate instead.
|
||||
///
|
||||
/// * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
///
|
||||
/// * returned vector is four u32 lengths, little-endian, 16 bytes total.
|
||||
#[wasm_bindgen]
|
||||
pub fn ext_mldsa_sizes(scheme: u32) -> Vec<u8> {
|
||||
dispatch!(scheme, dsa, _hd, {
|
||||
let sizes: [u32; 4] = [
|
||||
dsa::PUBLICKEYBYTES as u32,
|
||||
dsa::SECRETKEYBYTES as u32,
|
||||
dsa::SIGNBYTES as u32,
|
||||
(dsa::SIGNBYTES + dsa::PUBLICKEYBYTES) as u32,
|
||||
];
|
||||
|
||||
sizes.iter().flat_map(|n| n.to_le_bytes()).collect()
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether `scheme` names a parameter set this build supports.
|
||||
///
|
||||
/// `dispatch!` falls back to ML-DSA-87 for anything unrecognised, which is the
|
||||
/// right default but a poor way to discover a typo. Callers that accept a scheme
|
||||
/// from storage or from a user should check here first.
|
||||
#[wasm_bindgen]
|
||||
pub fn ext_mldsa_is_scheme(scheme: u32) -> bool {
|
||||
scheme == crate::scheme::ML_DSA_87 || scheme == crate::scheme::ML_DSA_65
|
||||
}
|
||||
|
||||
/// Generate a keypair from 32 bytes of entropy.
|
||||
///
|
||||
/// This is FIPS 204 `ML-DSA.KeyGen_internal` with no Quantus-specific step: the
|
||||
/// crate expands the seed as `SHAKE256(seed ‖ k ‖ ℓ)`, so the parameter set is
|
||||
/// absorbed into the expansion and the same 32 bytes yield independent keys per
|
||||
/// scheme. That is why the dev accounts (`[0u8; 32]`, `[1u8; 32]`, `[2u8; 32]`)
|
||||
/// and HD-derived accounts can share this one entry point.
|
||||
///
|
||||
/// * seed: UIntArray with 32 elements
|
||||
/// * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
///
|
||||
/// * returned vector is the secret key followed by the public key, matching the
|
||||
/// ordering `ext_ed_from_seed` uses. Split it at the secret length from
|
||||
/// `ext_mldsa_sizes`.
|
||||
#[wasm_bindgen]
|
||||
pub fn ext_mldsa_from_seed(seed: &[u8], scheme: u32) -> Result<Vec<u8>, JsError> {
|
||||
mldsa_from_seed(seed, scheme).map_err(|e| JsError::new(&e))
|
||||
}
|
||||
|
||||
/// The body of [`ext_mldsa_from_seed`], without the binding layer.
|
||||
///
|
||||
/// Split out because `JsError` cannot be constructed on a non-wasm target — it
|
||||
/// panics with "cannot call wasm-bindgen imported functions on non-wasm targets" —
|
||||
/// so anything that returns one is untestable by `cargo test`. The error paths are
|
||||
/// exactly what most needs testing, so the logic lives here and the exported
|
||||
/// wrapper does nothing but translate.
|
||||
pub fn mldsa_from_seed(seed: &[u8], scheme: u32) -> Result<Vec<u8>, String> {
|
||||
if seed.len() != 32 {
|
||||
return Err("expected a 32 byte seed".into());
|
||||
}
|
||||
|
||||
// `SensitiveBytes32::from` takes the buffer mutably and the crate zeroes it
|
||||
// after use, so the copy we hand it is destroyed rather than left on the
|
||||
// stack. Do not replace this with a by-value clone of `seed`.
|
||||
let mut entropy = [0u8; 32];
|
||||
|
||||
entropy.copy_from_slice(seed);
|
||||
|
||||
let mut entropy = qp_rusty_crystals_dilithium::SensitiveBytes32::from(&mut entropy);
|
||||
|
||||
dispatch!(scheme, dsa, _hd, {
|
||||
let pair = dsa::Keypair::generate(&mut entropy);
|
||||
let mut out = pair.secret().to_bytes().to_vec();
|
||||
|
||||
out.extend_from_slice(&pair.public().to_bytes());
|
||||
|
||||
Ok(out)
|
||||
})
|
||||
}
|
||||
|
||||
/// Sign a message under a FIPS 204 context.
|
||||
///
|
||||
/// Signing is deterministic — no hedging randomness — because that is what the
|
||||
/// runtime does (`hedge: None`), and a wallet that hedged would produce a
|
||||
/// different signature each time for the same input, which makes the
|
||||
/// byte-for-byte agreement tests in quantus/wasm#2 impossible to write.
|
||||
///
|
||||
/// `ctx` is domain separation and it is **not** optional in practice: extrinsics
|
||||
/// on spec >= 148 are verified under `QUANTUS_EXTRINSIC`, earlier specs under the
|
||||
/// empty context, and a signature made under the wrong one is valid, rejected by
|
||||
/// the chain, and indistinguishable locally. The caller chooses; this function
|
||||
/// does not guess.
|
||||
///
|
||||
/// * secret: UIntArray, secret-key length for the scheme
|
||||
/// * public: UIntArray, public-key length for the scheme
|
||||
/// * message: arbitrary length UIntArray
|
||||
/// * ctx: UIntArray, at most 255 elements; empty for no context
|
||||
/// * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
///
|
||||
/// * returned vector is the signature alone. The runtime's wire format is
|
||||
/// `signature ‖ public`; concatenating is the caller's job because only the
|
||||
/// caller knows whether it wants the wire form or the bare signature.
|
||||
#[wasm_bindgen]
|
||||
pub fn ext_mldsa_sign(secret: &[u8], public: &[u8], message: &[u8], ctx: &[u8], scheme: u32) -> Result<Vec<u8>, JsError> {
|
||||
mldsa_sign(secret, public, message, ctx, scheme).map_err(|e| JsError::new(&e))
|
||||
}
|
||||
|
||||
/// The body of [`ext_mldsa_sign`]. See [`mldsa_from_seed`] for why this is split.
|
||||
pub fn mldsa_sign(secret: &[u8], public: &[u8], message: &[u8], ctx: &[u8], scheme: u32) -> Result<Vec<u8>, String> {
|
||||
if ctx.len() > 255 {
|
||||
return Err("context must be at most 255 bytes".into());
|
||||
}
|
||||
|
||||
dispatch!(scheme, dsa, _hd, {
|
||||
// `from_parts` re-derives the public key from the secret and rejects a
|
||||
// mismatch, so a corrupted or mixed-up pair fails here rather than
|
||||
// producing a signature that silently will not verify.
|
||||
let secret = dsa::SecretKey::from_bytes(secret)
|
||||
.map_err(|_| "invalid secret key".to_string())?;
|
||||
let public = dsa::PublicKey::from_bytes(public)
|
||||
.map_err(|_| "invalid public key".to_string())?;
|
||||
let pair = dsa::Keypair::from_parts(secret, public)
|
||||
.map_err(|_| "secret and public key do not correspond".to_string())?;
|
||||
|
||||
pair
|
||||
.sign(message, Some(ctx), None)
|
||||
.map(|sig| sig.to_vec())
|
||||
.map_err(|_| "signing failed".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Verify a signature against a message and public key under a context.
|
||||
///
|
||||
/// * public: UIntArray, public-key length for the scheme
|
||||
/// * message: arbitrary length UIntArray
|
||||
/// * signature: UIntArray, signature length for the scheme
|
||||
/// * ctx: UIntArray, at most 255 elements; empty for no context
|
||||
/// * scheme: 0 for ML-DSA-87, 1 for ML-DSA-65
|
||||
#[wasm_bindgen]
|
||||
pub fn ext_mldsa_verify(public: &[u8], message: &[u8], signature: &[u8], ctx: &[u8], scheme: u32) -> bool {
|
||||
if ctx.len() > 255 {
|
||||
return false;
|
||||
}
|
||||
|
||||
dispatch!(scheme, dsa, _hd, {
|
||||
match dsa::PublicKey::from_bytes(public) {
|
||||
Ok(public) => public.verify(message, signature, Some(ctx)),
|
||||
Err(_) => false
|
||||
}
|
||||
})
|
||||
}
|
||||
22
packages/quantus-crypto/src/rs/poseidon.rs
Normal file
22
packages/quantus-crypto/src/rs/poseidon.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
/// Poseidon2-over-Goldilocks hash of arbitrary bytes.
|
||||
///
|
||||
/// This is the account-id derivation. On Substrate an `AccountId32` *is* the
|
||||
/// public key; on Quantus it is `hash_bytes(public_key)`, which is why a Quantus
|
||||
/// signature has to carry its public key along — the address cannot give it back.
|
||||
///
|
||||
/// `qp_poseidon_core::hash_bytes` is `IdentifyAccount for DilithiumSigner` in the
|
||||
/// runtime, so this is the same function the chain uses to decide who signed
|
||||
/// something, reached through the same crate rather than a port of it.
|
||||
///
|
||||
/// * data: arbitrary length UIntArray
|
||||
///
|
||||
/// * returned vector is 32 bytes.
|
||||
#[wasm_bindgen]
|
||||
pub fn ext_poseidon_hash(data: &[u8]) -> Vec<u8> {
|
||||
qp_poseidon_core::hash_bytes(data).to_vec()
|
||||
}
|
||||
47
packages/quantus-crypto/src/rs/scheme.rs
Normal file
47
packages/quantus-crypto/src/rs/scheme.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Which ML-DSA parameter set a call refers to.
|
||||
//!
|
||||
//! The selector is the chain's own signature-enum variant index, not a private
|
||||
//! numbering: `DilithiumSignatureScheme::Dilithium87` is variant 0 and
|
||||
//! `Dilithium65` is variant 1, and that byte is what a signed extrinsic carries
|
||||
//! on the wire. Reusing it here means the number threaded through this API is
|
||||
//! the number that ends up in the extrinsic, so there is no mapping table to get
|
||||
//! backwards between here and `TYPE_PREFIX` in the keyring.
|
||||
|
||||
/// ML-DSA-87 — the legacy scheme, used by accounts created before the scheme was
|
||||
/// recorded, and by the dev-genesis accounts.
|
||||
pub const ML_DSA_87: u32 = 0;
|
||||
|
||||
/// ML-DSA-65 — what new accounts use.
|
||||
pub const ML_DSA_65: u32 = 1;
|
||||
|
||||
/// Runs `$body` with `$dsa` and `$hd` bound to the parameter-set modules named by
|
||||
/// `$scheme`.
|
||||
///
|
||||
/// An unrecognised selector resolves to ML-DSA-87 rather than panicking: 0 is the
|
||||
/// legacy scheme and the safest thing an out-of-range value can mean. Callers
|
||||
/// that care validate first — see `ext_mldsa_is_scheme`.
|
||||
macro_rules! dispatch {
|
||||
($scheme:expr, $dsa:ident, $hd:ident, $body:block) => {
|
||||
match $scheme {
|
||||
$crate::scheme::ML_DSA_65 => {
|
||||
#[allow(unused_imports)]
|
||||
use qp_rusty_crystals_dilithium::ml_dsa_65 as $dsa;
|
||||
#[allow(unused_imports)]
|
||||
use qp_rusty_crystals_hdwallet::ml_dsa_65 as $hd;
|
||||
$body
|
||||
},
|
||||
_ => {
|
||||
#[allow(unused_imports)]
|
||||
use qp_rusty_crystals_dilithium::ml_dsa_87 as $dsa;
|
||||
#[allow(unused_imports)]
|
||||
use qp_rusty_crystals_hdwallet::ml_dsa_87 as $hd;
|
||||
$body
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use dispatch;
|
||||
303
packages/quantus-crypto/src/rs/tests.rs
Normal file
303
packages/quantus-crypto/src/rs/tests.rs
Normal file
@@ -0,0 +1,303 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! Conformance against the chain, not against ourselves.
|
||||
//!
|
||||
//! Every expected value here came from an independent implementation — the
|
||||
//! `quantus` CLI 2.2.2 — and not from running this crate and writing down what
|
||||
//! it said. A test that pins our own output would keep passing through exactly
|
||||
//! the kind of drift these exist to catch.
|
||||
//!
|
||||
//! Addresses are pinned as raw account-id bytes rather than SS58 strings so this
|
||||
//! file needs no base58 dependency; the SS58 rendering at prefix 189 is pinned on
|
||||
//! the JS side, which is where it is actually used.
|
||||
|
||||
use crate::{hdwallet::{mldsa_derive, wormhole_addresses}, mldsa::*, poseidon::*, scheme::*};
|
||||
|
||||
/// FIPS 204 context for on-chain extrinsic signatures, spec >= 148.
|
||||
/// `chain:primitives/dilithium-crypto/src/signing_context.rs`.
|
||||
const EXTRINSIC: &[u8] = b"QUANTUS_EXTRINSIC";
|
||||
|
||||
fn account_of(seed_fill: u8, scheme: u32) -> Vec<u8> {
|
||||
let pair = mldsa_from_seed(&[seed_fill; 32], scheme).expect("keygen");
|
||||
let secret_len = secret_len(scheme);
|
||||
|
||||
ext_poseidon_hash(&pair[secret_len..])
|
||||
}
|
||||
|
||||
fn secret_len(scheme: u32) -> usize {
|
||||
let sizes = ext_mldsa_sizes(scheme);
|
||||
|
||||
u32::from_le_bytes(sizes[4..8].try_into().unwrap()) as usize
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sizes_match_the_parameter_sets() {
|
||||
// [public, secret, signature, signature_with_public]
|
||||
let s87: Vec<u32> = ext_mldsa_sizes(ML_DSA_87).chunks(4).map(|c| u32::from_le_bytes(c.try_into().unwrap())).collect();
|
||||
let s65: Vec<u32> = ext_mldsa_sizes(ML_DSA_65).chunks(4).map(|c| u32::from_le_bytes(c.try_into().unwrap())).collect();
|
||||
|
||||
assert_eq!(s87, vec![2592, 4896, 4627, 7219]);
|
||||
assert_eq!(s65, vec![1952, 4032, 3309, 5261]);
|
||||
}
|
||||
|
||||
/// The dev accounts endowed at genesis, from `chain:primitives/dilithium-crypto/src/pair.rs`.
|
||||
///
|
||||
/// Expected values are the account ids behind the SS58 addresses that
|
||||
/// `quantus developer create-test-wallets` prints:
|
||||
///
|
||||
/// ```text
|
||||
/// crystal_alice qzk1Nxai3dZD9Cn5kwGcgL6mKxsfxwqdis7kDQJ52aJS2vSn7
|
||||
/// dilithium_bob qzkYEQv8tQsmniZYdame3Cku18RL5g9bGK9Pdydq5TMPdpE3y
|
||||
/// crystal_charlie qzntBpmqHZF1jxC8KJKpuxcYuHST892jyXBqRctpAxd1WQ9BL
|
||||
/// ```
|
||||
///
|
||||
/// They are ML-DSA-87 and come from the seed directly with no HD derivation, so
|
||||
/// this pins the legacy scheme and the raw-seed path in one go.
|
||||
#[test]
|
||||
fn dev_account_ids_match_the_cli() {
|
||||
assert_eq!(
|
||||
hex(&account_of(0, ML_DSA_87)),
|
||||
"1883df2ae47d1fd428a6b8237ad7b59cf0facccaacac4541ef7758be44b3c333",
|
||||
"crystal_alice"
|
||||
);
|
||||
assert_eq!(
|
||||
hex(&account_of(1, ML_DSA_87)),
|
||||
"300bb607ba60e89461d2f9005668231ceb30237b33db53a614164b8590965519",
|
||||
"dilithium_bob"
|
||||
);
|
||||
assert_eq!(
|
||||
hex(&account_of(2, ML_DSA_87)),
|
||||
"97bc5f2db1efa23fb71f6737fcb26e41e448aff07447011369df81ce43555465",
|
||||
"crystal_charlie"
|
||||
);
|
||||
}
|
||||
|
||||
/// The same 32 bytes must give different keys per parameter set — FIPS 204
|
||||
/// absorbs `(k, ℓ)` into the seed expansion. If these ever collided it would mean
|
||||
/// the scheme selector was being ignored somewhere.
|
||||
#[test]
|
||||
fn schemes_are_independent_for_the_same_seed() {
|
||||
assert_ne!(account_of(0, ML_DSA_87), account_of(0, ML_DSA_65));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signs_and_verifies_under_the_extrinsic_context() {
|
||||
for scheme in [ML_DSA_87, ML_DSA_65] {
|
||||
let pair = mldsa_from_seed(&[7u8; 32], scheme).expect("keygen");
|
||||
let (secret, public) = pair.split_at(secret_len(scheme));
|
||||
let message = b"the payload the chain will see";
|
||||
let signature = mldsa_sign(secret, public, message, EXTRINSIC, scheme).expect("sign");
|
||||
|
||||
assert!(ext_mldsa_verify(public, message, &signature, EXTRINSIC, scheme));
|
||||
|
||||
// The whole point of the context. A signature made for an extrinsic must
|
||||
// not verify as anything else, and vice versa — this is what makes the
|
||||
// spec-148 boundary detectable instead of a silent chain rejection.
|
||||
assert!(!ext_mldsa_verify(public, message, &signature, b"", scheme));
|
||||
assert!(!ext_mldsa_verify(public, b"tampered", &signature, EXTRINSIC, scheme));
|
||||
}
|
||||
}
|
||||
|
||||
/// The runtime signs with `hedge: None`. If this crate ever introduced hedging
|
||||
/// randomness the golden vectors in quantus/wasm#2 would become unwritable, and
|
||||
/// nothing else would notice.
|
||||
#[test]
|
||||
fn signing_is_deterministic() {
|
||||
let pair = mldsa_from_seed(&[9u8; 32], ML_DSA_65).expect("keygen");
|
||||
let (secret, public) = pair.split_at(secret_len(ML_DSA_65));
|
||||
let once = mldsa_sign(secret, public, b"m", EXTRINSIC, ML_DSA_65).expect("sign");
|
||||
let twice = mldsa_sign(secret, public, b"m", EXTRINSIC, ML_DSA_65).expect("sign");
|
||||
|
||||
assert_eq!(once, twice);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_bad_input() {
|
||||
assert!(mldsa_from_seed(&[0u8; 16], ML_DSA_65).is_err(), "short seed");
|
||||
|
||||
let pair = mldsa_from_seed(&[1u8; 32], ML_DSA_65).expect("keygen");
|
||||
let (secret, public) = pair.split_at(secret_len(ML_DSA_65));
|
||||
|
||||
assert!(mldsa_sign(secret, public, b"m", &[0u8; 256], ML_DSA_65).is_err(), "context > 255");
|
||||
assert!(mldsa_sign(&secret[1..], public, b"m", EXTRINSIC, ML_DSA_65).is_err(), "truncated secret");
|
||||
|
||||
// A pair whose halves do not correspond must fail at import rather than
|
||||
// produce a signature that silently will not verify.
|
||||
let other = mldsa_from_seed(&[2u8; 32], ML_DSA_65).expect("keygen");
|
||||
let other_public = &other[secret_len(ML_DSA_65)..];
|
||||
|
||||
assert!(mldsa_sign(secret, other_public, b"m", EXTRINSIC, ML_DSA_65).is_err(), "mismatched pair");
|
||||
}
|
||||
|
||||
/// The well-known Substrate development phrase. Public by design — it is in
|
||||
/// polkadot-sdk, in polkadot-js, and in every tutorial — so pinning it here
|
||||
/// commits no secret. Any account it derives is assumed compromised.
|
||||
const DEV_PHRASE: &str = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
|
||||
|
||||
/// HD derivation at the Quantus BIP44 path, cross-checked against
|
||||
/// `quantus wallet import --mnemonic-file <DEV_PHRASE> --scheme <s>`, which
|
||||
/// printed:
|
||||
///
|
||||
/// ```text
|
||||
/// ml-dsa-65 m/44'/189189'/0'/0'/1' qzq29m9WvneDAeXbtgueKCREtNe1rVVs6bXSMLmjr6shqvwq6
|
||||
/// ml-dsa-87 m/44'/189189'/0'/0'/0' qzjrYTUnnE5NduTZKxe9dESCMTZg7nTueKM3bwhnkRdD1iYV4
|
||||
/// ```
|
||||
///
|
||||
/// This pins the whole derivation chain at once: BIP39 to a 64-byte seed (*not*
|
||||
/// Substrate's `mnemonicToMiniSecret`), the HMAC-SHA512 walk keyed with
|
||||
/// "Dilithium seed", the trailing hardened index carrying the scheme, and the
|
||||
/// Poseidon2 account-id hash on the end.
|
||||
#[test]
|
||||
fn hd_derivation_matches_the_cli() {
|
||||
let cases = [
|
||||
(ML_DSA_65, "m/44'/189189'/0'/0'/1'", "f647dbdefebcfcf726ba078a83481ffc6f4f33004fdfb4cedacf5a5391bc8f00"),
|
||||
(ML_DSA_87, "m/44'/189189'/0'/0'/0'", "11c6a314e003cdee3dc51cf6569175360141578d054c38d7a70840a65cc0e990")
|
||||
];
|
||||
|
||||
for (scheme, path, expected) in cases {
|
||||
let pair = mldsa_derive(DEV_PHRASE, "", path, scheme).expect("derive");
|
||||
let account = ext_poseidon_hash(&pair[secret_len(scheme)..]);
|
||||
|
||||
assert_eq!(hex(&account), expected, "{path}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Lattice keys have no public derivability, so the crate rejects unhardened
|
||||
/// paths outright rather than inventing a meaning for them. A wallet that
|
||||
/// silently hardened a soft path would put funds at an address the user did not
|
||||
/// ask for.
|
||||
#[test]
|
||||
fn derivation_rejects_bad_input() {
|
||||
assert!(mldsa_derive(DEV_PHRASE, "", "m/44'/189189'/0'/0'/1", ML_DSA_65).is_err(), "unhardened");
|
||||
assert!(mldsa_derive("not a mnemonic at all", "", "m/44'/189189'/0'/0'/1'", ML_DSA_65).is_err(), "bad phrase");
|
||||
assert!(mldsa_derive(DEV_PHRASE, "", "not a path", ML_DSA_65).is_err(), "bad path");
|
||||
}
|
||||
|
||||
/// A BIP39 passphrase must change the result, and an empty string must mean
|
||||
/// "no passphrase" rather than "a passphrase that happens to be empty" — the
|
||||
/// two are the same in BIP39 but the crate's signature distinguishes them, and
|
||||
/// normalising in the wrong direction would silently fork every address.
|
||||
#[test]
|
||||
fn passphrase_is_honoured_and_empty_means_none() {
|
||||
let path = "m/44'/189189'/0'/0'/1'";
|
||||
let none = mldsa_derive(DEV_PHRASE, "", path, ML_DSA_65).expect("derive");
|
||||
let with = mldsa_derive(DEV_PHRASE, "hunter2", path, ML_DSA_65).expect("derive");
|
||||
|
||||
assert_ne!(none, with);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheme_validation() {
|
||||
assert!(ext_mldsa_is_scheme(ML_DSA_87));
|
||||
assert!(ext_mldsa_is_scheme(ML_DSA_65));
|
||||
assert!(!ext_mldsa_is_scheme(2));
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||
}
|
||||
|
||||
/// The chain node's own wormhole vector, `node/src/tests/data/quantus_key_test_data.rs`:
|
||||
/// `TEST_MNEMONIC` at `m/44'/189189189'/0'/0'/0'` is `TEST_WORMHOLE_ADDRESS`,
|
||||
/// `qzpWh4AEtsgCyEbv4WBgFWnB9bcdF2L2jVDuyjXP9mSTyBaeU`. The mobile wallet's SDK
|
||||
/// pins the same pair (`generate_keys_test.dart`), so three implementations agree.
|
||||
#[test]
|
||||
fn wormhole_address_matches_the_node() {
|
||||
const MNEMONIC: &str = "orchard answer curve patient visual flower maze noise retreat penalty cage small earth domain scan pitch bottom crunch theme club client swap slice raven";
|
||||
|
||||
let batch = wormhole_addresses(MNEMONIC, "", 0, 0, 0, 3).expect("derive");
|
||||
|
||||
assert_eq!(batch.len(), 96);
|
||||
assert_eq!(hex(&batch[..32]), "dfcfd6e59c75d208e84f54a887537bcf7b04265790ec79960bf49de123404d0e");
|
||||
|
||||
// A batch is the same as asking for each index on its own: the seed is
|
||||
// stretched once, but every address still gets its own path.
|
||||
for i in 0..3u32 {
|
||||
let one = wormhole_addresses(MNEMONIC, "", 0, 0, i, 1).expect("derive one");
|
||||
|
||||
assert_eq!(one, batch[(i as usize * 32)..(i as usize + 1) * 32].to_vec());
|
||||
}
|
||||
|
||||
// Receive and change branches, and account indices, are different addresses.
|
||||
assert_ne!(wormhole_addresses(MNEMONIC, "", 0, 1, 0, 1).unwrap(), batch[..32].to_vec());
|
||||
assert_ne!(wormhole_addresses(MNEMONIC, "", 1, 0, 0, 1).unwrap(), batch[..32].to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wormhole_addresses_refuse_unbounded_requests() {
|
||||
const MNEMONIC: &str = "orchard answer curve patient visual flower maze noise retreat penalty cage small earth domain scan pitch bottom crunch theme club client swap slice raven";
|
||||
|
||||
assert!(wormhole_addresses(MNEMONIC, "", 0, 0, 0, 1001).is_err());
|
||||
assert!(wormhole_addresses(MNEMONIC, "", 0, 0, 0x7fff_ffff, 2).is_err());
|
||||
assert!(wormhole_addresses("not a mnemonic", "", 0, 0, 0, 1).is_err());
|
||||
}
|
||||
|
||||
/// The port of the nullifier agrees with the chain's circuit crate.
|
||||
///
|
||||
/// `qp_wormhole_circuit::nullifier::Nullifier::from_preimage` is what the proof
|
||||
/// commits to and what `Wormhole::UsedNullifiers` records, so it is the
|
||||
/// reference. Secrets span the edge the lossy 8-bytes-per-felt encoding cares
|
||||
/// about (limbs at and above the Goldilocks prime), and transfer counts span
|
||||
/// both 32-bit limbs.
|
||||
#[test]
|
||||
fn nullifier_matches_the_circuit() {
|
||||
use crate::hdwallet::nullifier;
|
||||
use qp_wormhole_circuit::nullifier::Nullifier;
|
||||
use qp_zk_circuits_common::utils::{digest_to_bytes, BytesDigest};
|
||||
|
||||
let secrets: Vec<[u8; 32]> = vec![
|
||||
[0u8; 32],
|
||||
[0xff; 32],
|
||||
core::array::from_fn(|i| i as u8),
|
||||
core::array::from_fn(|i| (i as u8).wrapping_mul(97).wrapping_add(13)),
|
||||
// every limb is the Goldilocks prime 2^64 - 2^32 + 1, big-endian
|
||||
[0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x01].repeat(4).try_into().unwrap(),
|
||||
];
|
||||
let counts = [0u64, 1, 41_683, u32::MAX as u64, 1u64 << 32, u64::MAX];
|
||||
let mut compared = 0;
|
||||
|
||||
for secret in &secrets {
|
||||
for &count in &counts {
|
||||
let digest = BytesDigest::try_from(*secret);
|
||||
// The circuit's BytesDigest refuses a non-canonical limb; where it
|
||||
// does, the chain can never produce that secret's nullifier either.
|
||||
let Ok(digest) = digest else { continue };
|
||||
let expected = digest_to_bytes(Nullifier::from_preimage(digest, count).hash);
|
||||
|
||||
assert_eq!(hex(&nullifier(secret, count)), hex(expected.as_ref()), "secret {} count {count}", hex(secret));
|
||||
compared += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Skipping is for the non-canonical edge only; a test that compared
|
||||
// nothing would pass just as well.
|
||||
assert!(compared >= 18, "only {compared} cases compared");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wormhole_nullifiers_follow_each_address_secret() {
|
||||
use crate::hdwallet::{nullifier, wormhole_nullifiers};
|
||||
use qp_rusty_crystals_hdwallet::derive_wormhole_from_mnemonic;
|
||||
|
||||
const MNEMONIC: &str = "orchard answer curve patient visual flower maze noise retreat penalty cage small earth domain scan pitch bottom crunch theme club client swap slice raven";
|
||||
|
||||
// addresses 2 and 3 on the change branch, transfer counts 5..8
|
||||
let batch = wormhole_nullifiers(MNEMONIC, "", 0, 1, 2, 2, 5, 3).unwrap();
|
||||
|
||||
assert_eq!(batch.len(), 2 * 3 * 32);
|
||||
|
||||
for (a, index) in [2u32, 3].into_iter().enumerate() {
|
||||
let pair = derive_wormhole_from_mnemonic(MNEMONIC, None, &format!("m/44'/189189189'/0'/1'/{index}'")).unwrap();
|
||||
|
||||
for c in 0..3usize {
|
||||
let at = (a * 3 + c) * 32;
|
||||
|
||||
assert_eq!(batch[at..at + 32].to_vec(), nullifier(pair.secret().as_bytes(), 5 + c as u64).to_vec(), "address {index} count {}", 5 + c);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(wormhole_nullifiers(MNEMONIC, "", 0, 0, 0, 40, 0, 2_501).is_err());
|
||||
assert!(wormhole_nullifiers(MNEMONIC, "", 0, 0, 0, 1, u64::MAX, 2).is_err());
|
||||
assert!(wormhole_nullifiers(MNEMONIC, "", 0, 0, 0x7fff_ffff, 2, 0, 1).is_err());
|
||||
}
|
||||
46
packages/quantus-crypto/src/scheme.ts
Normal file
46
packages/quantus-crypto/src/scheme.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2026 @quantus/crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Which ML-DSA parameter set a call refers to.
|
||||
*
|
||||
* These are the chain's own `DilithiumSignatureScheme` variant indices, not a
|
||||
* private numbering — the value here is the byte a signed extrinsic carries on
|
||||
* the wire. Keeping them identical means the keyring's `TYPE_PREFIX` is the
|
||||
* identity function on a scheme, with no table to get backwards.
|
||||
*/
|
||||
export enum Scheme {
|
||||
/** ML-DSA-87. Legacy: accounts created before the scheme was recorded, and the dev-genesis accounts. */
|
||||
MlDsa87 = 0,
|
||||
/** ML-DSA-65. What new accounts use. */
|
||||
MlDsa65 = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* The name each scheme is stored under, matching `quantus-cli` and the mobile
|
||||
* wallet so a wallet exported from one tool imports into another.
|
||||
*/
|
||||
export const SCHEME_NAME: Record<Scheme, string> = {
|
||||
[Scheme.MlDsa87]: 'ml-dsa-87',
|
||||
[Scheme.MlDsa65]: 'ml-dsa-65'
|
||||
};
|
||||
|
||||
/**
|
||||
* FIPS 204 context for on-chain extrinsic signatures.
|
||||
*
|
||||
* Only from spec 148 onward — earlier runtimes verify under the empty context,
|
||||
* and a signature made under the wrong one is valid, rejected by the chain, and
|
||||
* indistinguishable locally. Callers pass the spec version and get the right
|
||||
* answer from {@link contextForSpec}; nothing here guesses.
|
||||
*/
|
||||
export const EXTRINSIC_CONTEXT = new TextEncoder().encode('QUANTUS_EXTRINSIC');
|
||||
|
||||
/** First spec version that verifies extrinsics under {@link EXTRINSIC_CONTEXT}. */
|
||||
export const EXTRINSIC_MIN_SPEC = 148;
|
||||
|
||||
/** The signing context a runtime at `specVersion` expects. */
|
||||
export function contextForSpec (specVersion: number): Uint8Array {
|
||||
return specVersion >= EXTRINSIC_MIN_SPEC
|
||||
? EXTRINSIC_CONTEXT
|
||||
: new Uint8Array();
|
||||
}
|
||||
76
packages/quantus-crypto/test/consumer.mjs
Normal file
76
packages/quantus-crypto/test/consumer.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
// 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, WormholeBranch, wormholeAddresses, wormholeNullifiers
|
||||
} 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);
|
||||
|
||||
// wormhole: the chain node's TEST_WORMHOLE_ADDRESS (qzpWh4AEtsgCyEbv4WBgFWnB9bcdF2L2jVDuyjXP9mSTyBaeU)
|
||||
const NODE_PHRASE = 'orchard answer curve patient visual flower maze noise retreat penalty cage small earth domain scan pitch bottom crunch theme club client swap slice raven';
|
||||
const wh = wormholeAddresses(NODE_PHRASE, '', 0, WormholeBranch.Receive, 0, 2);
|
||||
eq('wormhole address count', wh.length, 2);
|
||||
eq('wormhole receive 0 is the node test address',
|
||||
Buffer.from(wh[0]).toString('hex'),
|
||||
'dfcfd6e59c75d208e84f54a887537bcf7b04265790ec79960bf49de123404d0e');
|
||||
eq('change branch differs', Buffer.from(wormholeAddresses(NODE_PHRASE, '', 0, WormholeBranch.Change, 0, 1)[0]).toString('hex') !== Buffer.from(wh[0]).toString('hex'), true);
|
||||
|
||||
// nullifiers: shape, determinism, and the cost of a wallet account's precompute
|
||||
const n = wormholeNullifiers(NODE_PHRASE, '', 0, WormholeBranch.Receive, 0, 2, 7, 3);
|
||||
eq('nullifier shape', `${n.length}x${n[0].length}x${n[0][0].length}`, '2x3x32');
|
||||
eq('nullifiers differ by count', Buffer.from(n[0][0]).equals(Buffer.from(n[0][1])), false);
|
||||
eq('nullifiers differ by address', Buffer.from(n[0][0]).equals(Buffer.from(n[1][0])), false);
|
||||
eq('a sub-range agrees', Buffer.from(wormholeNullifiers(NODE_PHRASE, '', 0, WormholeBranch.Receive, 1, 1, 8, 1)[0][0]).toString('hex'), Buffer.from(n[1][1]).toString('hex'));
|
||||
const t0 = performance.now();
|
||||
wormholeNullifiers(NODE_PHRASE, '', 0, WormholeBranch.Receive, 0, 20, 0, 256);
|
||||
wormholeNullifiers(NODE_PHRASE, '', 0, WormholeBranch.Change, 0, 20, 0, 256);
|
||||
console.log(` 40 addresses x 256 counts: ${Math.round(performance.now() - t0)} ms`);
|
||||
|
||||
process.exit(fail ? 1 : 0);
|
||||
1
packages/quantus-crypto/test/probe/.gitignore
vendored
Normal file
1
packages/quantus-crypto/test/probe/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
vendor/
|
||||
69
packages/quantus-crypto/test/probe/README.md
Normal file
69
packages/quantus-crypto/test/probe/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Browser probes
|
||||
|
||||
`cargo test` and the consumer test both run in node, and node is neither a
|
||||
browser nor a service worker. These two probes cover what node cannot: does the
|
||||
WASM instantiate with **no DOM**, under the **extension's own CSP**, and what does
|
||||
a cold start cost.
|
||||
|
||||
Build first with `./scripts/build-quantus.sh`, stage `vendor/` (below), then run
|
||||
`node serve.mjs` from this directory.
|
||||
|
||||
## `index.html` + `worker.js` — automated
|
||||
|
||||
A module Worker, served with the exact `extension_pages` CSP from both extension
|
||||
manifests:
|
||||
|
||||
```
|
||||
script-src 'self' 'wasm-unsafe-eval'; object-src 'self'
|
||||
```
|
||||
|
||||
A module Worker has no `window` and no `document`, which is the property that
|
||||
matters — an MV3 service worker has neither either. Result on Firefox:
|
||||
|
||||
```
|
||||
hasDOM: false hasWindow: false
|
||||
initWasm: ok (9.0 ms cold)
|
||||
keygen: 1.0 ms
|
||||
account: matches quantus-cli
|
||||
sign: 3.0 ms (4627 bytes)
|
||||
verify: 1.0 ms ok
|
||||
ctx sep: ok (rejected under spec-147 ctx)
|
||||
```
|
||||
|
||||
9 ms to base64-decode 146 KB, zlib-inflate it to 234 KB and instantiate. That is
|
||||
the number the MV3 lifetime question turns on: a service worker killed between
|
||||
messages pays this on every wake, and 9 ms is not a problem. Those are ML-DSA-87
|
||||
timings — the larger parameter set — so ML-DSA-65 is cheaper still.
|
||||
|
||||
The CSP is genuinely enforced here, not merely declared: an earlier version of
|
||||
this page used an inline `<script>` and Firefox blocked it, which is why
|
||||
`main.js` exists as a separate file.
|
||||
|
||||
## `manifest.json` + `sw.js` — manual
|
||||
|
||||
The real thing: an MV3 extension whose background service worker imports the
|
||||
package at module scope and signs once. Loading an unpacked extension needs an OS
|
||||
file dialog, so this cannot be driven from a script — load it by hand via
|
||||
`chrome://extensions` → Developer mode → Load unpacked, then click the toolbar
|
||||
icon.
|
||||
|
||||
It covers what the Worker probe cannot: `chrome.runtime` messaging, and the
|
||||
actual MV3 kill-and-restart lifecycle rather than a stand-in for it.
|
||||
|
||||
## Staging `vendor/`
|
||||
|
||||
Both probes import from `./vendor/`, which is not checked in. Populate it from a
|
||||
build:
|
||||
|
||||
```sh
|
||||
mkdir -p vendor
|
||||
cp -r ../../build vendor/quantus-crypto
|
||||
cp -r ../../../wasm-util/build vendor/wasm-util
|
||||
sed -i "s|from '@polkadot/wasm-util/base64'|from '../wasm-util/base64.js'|; \
|
||||
s|from '@polkadot/wasm-util/fflate'|from '../wasm-util/fflate.js'|" \
|
||||
vendor/quantus-crypto/init.js
|
||||
```
|
||||
|
||||
The rewrite is needed because a browser cannot resolve bare specifiers. A real
|
||||
extension build does this with a bundler; here it is one `sed` rather than a
|
||||
build step, because the probe exists to test the WASM, not the bundler.
|
||||
5
packages/quantus-crypto/test/probe/index.html
Normal file
5
packages/quantus-crypto/test/probe/index.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<!doctype html><meta charset="utf-8"><title>quantus-crypto worker probe</title>
|
||||
<body style="font:13px ui-monospace,monospace;padding:16px;background:#111;color:#ddd">
|
||||
<h3 style="font:600 14px system-ui">@quantus/crypto in a module Worker under the extension CSP</h3>
|
||||
<pre id="out">running…</pre>
|
||||
<script type="module" src="./main.js"></script>
|
||||
3
packages/quantus-crypto/test/probe/main.js
Normal file
3
packages/quantus-crypto/test/probe/main.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const w = new Worker('./worker.js', { type: 'module' });
|
||||
w.onmessage = (e) => { document.getElementById('out').textContent = e.data; };
|
||||
w.onerror = (e) => { document.getElementById('out').textContent = 'worker error: ' + (e.message || 'see console'); };
|
||||
11
packages/quantus-crypto/test/probe/manifest.json
Normal file
11
packages/quantus-crypto/test/probe/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "quantus-crypto MV3 probe",
|
||||
"version": "0.0.1",
|
||||
"description": "Loads @quantus/crypto in an MV3 service worker and signs once.",
|
||||
"background": { "service_worker": "sw.js", "type": "module" },
|
||||
"action": { "default_title": "probe", "default_popup": "popup.html" },
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
||||
}
|
||||
}
|
||||
4
packages/quantus-crypto/test/probe/popup.html
Normal file
4
packages/quantus-crypto/test/probe/popup.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<!doctype html><meta charset="utf-8"><title>quantus-crypto probe</title>
|
||||
<body style="font:13px system-ui;padding:12px;min-width:380px">
|
||||
<pre id="out">running…</pre>
|
||||
<script type="module" src="popup.js"></script>
|
||||
4
packages/quantus-crypto/test/probe/popup.js
Normal file
4
packages/quantus-crypto/test/probe/popup.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const out = document.getElementById('out');
|
||||
chrome.runtime.sendMessage({ probe: true }, (r) => {
|
||||
out.textContent = r ? r.text : `no response: ${chrome.runtime.lastError?.message}`;
|
||||
});
|
||||
15
packages/quantus-crypto/test/probe/serve.mjs
Normal file
15
packages/quantus-crypto/test/probe/serve.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
import http from 'node:http';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
const types = { '.html': 'text/html', '.js': 'text/javascript', '.json': 'application/json', '.wasm': 'application/wasm' };
|
||||
http.createServer((req, res) => {
|
||||
const p = path.join(process.cwd(), decodeURIComponent(req.url.split('?')[0]));
|
||||
const f = fs.existsSync(p) && fs.statSync(p).isDirectory() ? path.join(p, 'index.html') : p;
|
||||
if (!fs.existsSync(f)) { res.writeHead(404); return res.end('nope'); }
|
||||
res.writeHead(200, {
|
||||
'Content-Type': types[path.extname(f)] || 'application/octet-stream',
|
||||
// exactly the extension_pages CSP from both manifests
|
||||
'Content-Security-Policy': "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
||||
});
|
||||
fs.createReadStream(f).pipe(res);
|
||||
}).listen(8731, () => console.log('probe on http://127.0.0.1:8731'));
|
||||
52
packages/quantus-crypto/test/probe/sw.js
Normal file
52
packages/quantus-crypto/test/probe/sw.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// MV3 service worker. Imports @quantus/crypto at module scope — i.e. on every
|
||||
// cold start, which is what actually needs proving: the worker is killed between
|
||||
// messages and must re-instantiate a 146 KB inlined base64 wasm each time.
|
||||
import {
|
||||
accountFromPublicKey, contextForSpec, initWasm, keypairFromSeed,
|
||||
Scheme, sign, verify
|
||||
} from './vendor/quantus-crypto/index.js';
|
||||
|
||||
const hex = (u8) => [...u8].map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
function probe () {
|
||||
const lines = [];
|
||||
const t0 = performance.now();
|
||||
const err = initWasm();
|
||||
const tInit = performance.now() - t0;
|
||||
|
||||
lines.push(`context: ${typeof window === 'undefined' ? 'service worker (no DOM)' : 'page'}`);
|
||||
lines.push(`initWasm: ${err === null ? 'ok' : 'FAILED — ' + err} (${tInit.toFixed(1)} ms)`);
|
||||
|
||||
if (err) return lines.join('\n');
|
||||
|
||||
const t1 = performance.now();
|
||||
const pair = keypairFromSeed(new Uint8Array(32), Scheme.MlDsa87);
|
||||
const tKeygen = performance.now() - t1;
|
||||
const account = hex(accountFromPublicKey(pair.publicKey));
|
||||
const expected = '1883df2ae47d1fd428a6b8237ad7b59cf0facccaacac4541ef7758be44b3c333';
|
||||
|
||||
lines.push(`keygen: ${tKeygen.toFixed(1)} ms`);
|
||||
lines.push(`account: ${account.slice(0, 24)}… ${account === expected ? 'matches quantus-cli' : 'MISMATCH'}`);
|
||||
|
||||
const msg = new TextEncoder().encode('extrinsic payload');
|
||||
const ctx = contextForSpec(148);
|
||||
const t2 = performance.now();
|
||||
const sig = sign(msg, pair, ctx, Scheme.MlDsa87);
|
||||
const tSign = performance.now() - t2;
|
||||
|
||||
lines.push(`sign: ${tSign.toFixed(1)} ms (${sig.length} bytes)`);
|
||||
lines.push(`verify: ${verify(msg, sig, pair.publicKey, ctx, Scheme.MlDsa87) ? 'ok' : 'FAILED'}`);
|
||||
lines.push(`ctx sep: ${verify(msg, sig, pair.publicKey, contextForSpec(147), Scheme.MlDsa87) ? 'FAILED (verified under wrong ctx)' : 'ok (rejected under spec 147 ctx)'}`);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((_m, _s, respond) => {
|
||||
try {
|
||||
respond({ text: probe() });
|
||||
} catch (e) {
|
||||
respond({ text: `threw: ${e && e.message ? e.message : e}\n${e && e.stack ? e.stack : ''}` });
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
32
packages/quantus-crypto/test/probe/worker.js
Normal file
32
packages/quantus-crypto/test/probe/worker.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// A module Worker: no DOM, no window, same CSP as the extension pages.
|
||||
import { accountFromPublicKey, contextForSpec, initWasm, keypairFromSeed, Scheme, sign, verify } from './vendor/quantus-crypto/index.js';
|
||||
|
||||
const hex = (u8) => [...u8].map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
const lines = [];
|
||||
const t0 = performance.now();
|
||||
const err = initWasm();
|
||||
const tInit = performance.now() - t0;
|
||||
|
||||
lines.push(`hasDOM: ${typeof document !== 'undefined'} hasWindow: ${typeof window !== 'undefined'}`);
|
||||
lines.push(`initWasm: ${err === null ? 'ok' : 'FAILED - ' + err} (${tInit.toFixed(1)} ms cold)`);
|
||||
|
||||
if (err === null) {
|
||||
const t1 = performance.now();
|
||||
const pair = keypairFromSeed(new Uint8Array(32), Scheme.MlDsa87);
|
||||
const tKeygen = performance.now() - t1;
|
||||
const account = hex(accountFromPublicKey(pair.publicKey));
|
||||
lines.push(`keygen: ${tKeygen.toFixed(1)} ms`);
|
||||
lines.push(`account: ${account === '1883df2ae47d1fd428a6b8237ad7b59cf0facccaacac4541ef7758be44b3c333' ? 'matches quantus-cli' : 'MISMATCH ' + account}`);
|
||||
const msg = new TextEncoder().encode('extrinsic payload');
|
||||
const ctx = contextForSpec(148);
|
||||
const t2 = performance.now();
|
||||
const sig = sign(msg, pair, ctx, Scheme.MlDsa87);
|
||||
const tSign = performance.now() - t2;
|
||||
const t3 = performance.now();
|
||||
const ok = verify(msg, sig, pair.publicKey, ctx, Scheme.MlDsa87);
|
||||
const tVerify = performance.now() - t3;
|
||||
lines.push(`sign: ${tSign.toFixed(1)} ms (${sig.length} bytes)`);
|
||||
lines.push(`verify: ${tVerify.toFixed(1)} ms ${ok ? 'ok' : 'FAILED'}`);
|
||||
lines.push(`ctx sep: ${verify(msg, sig, pair.publicKey, contextForSpec(147), Scheme.MlDsa87) ? 'FAILED' : 'ok (rejected under spec-147 ctx)'}`);
|
||||
}
|
||||
postMessage(lines.join('\n'));
|
||||
18
packages/quantus-crypto/tsconfig.build.json
Normal file
18
packages/quantus-crypto/tsconfig.build.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "..",
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src",
|
||||
"emitDeclarationOnly": false
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"references": []
|
||||
}
|
||||
@@ -18,15 +18,15 @@
|
||||
"./packageDetect.cjs"
|
||||
],
|
||||
"type": "module",
|
||||
"version": "7.5.1",
|
||||
"version": "7.5.4",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@polkadot/wasm-util": "7.5.1",
|
||||
"@polkadot/wasm-util": "7.5.4",
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@polkadot/util": "^13.5.5",
|
||||
"@polkadot/x-randomvalues": "^13.5.5"
|
||||
"@polkadot/util": "^14.0.1",
|
||||
"@polkadot/x-randomvalues": "^14.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "*",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-bridge authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-bridge authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// A number of functions are "unsafe" and purposefully so - it is
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-bridge authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-bridge authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export * from './bridge.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-bridge authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-bridge authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import './packageDetect.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-bridge authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-bridge authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { InitFn, InitPromise, InitResult, WasmBaseInstance, WasmImports } from './types.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-bridge authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-bridge authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export * from './index.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017-2025 @polkadot/wasm-bridge authors & contributors
|
||||
// Copyright 2017-2026 @polkadot/wasm-bridge authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright 2017-2025 @polkadot/wasm-bridge authors & contributors
|
||||
// Copyright 2017-2026 @polkadot/wasm-bridge authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/wasm-bridge', path: 'auto', type: 'auto', version: '7.5.1' };
|
||||
export const packageInfo = { name: '@polkadot/wasm-bridge', path: 'auto', type: 'auto', version: '7.5.4' };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-bridge authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-bridge authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Use non-strong types instead of WasmImports which may not
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-bridge authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-bridge authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BridgeBase, WasmBaseInstance } from './types.js';
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
"./packageDetect.cjs"
|
||||
],
|
||||
"type": "module",
|
||||
"version": "7.5.1",
|
||||
"version": "7.5.4",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@polkadot/util": "^13.5.5"
|
||||
"@polkadot/util": "^14.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "*"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export { asmJsInit } from './cjs/data.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
const data = require('../data.js');
|
||||
|
||||
2
packages/wasm-crypto-asmjs/src/cjs/data.d.ts
vendored
2
packages/wasm-crypto-asmjs/src/cjs/data.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export declare const asmJsInit: null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
const asmJsInit = null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export declare const asmJsInit: null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export const asmJsInit = null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import './packageDetect.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export * from './index.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017-2025 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// Copyright 2017-2026 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright 2017-2025 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// Copyright 2017-2026 @polkadot/wasm-crypto-asmjs authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/wasm-crypto-asmjs', path: 'auto', type: 'auto', version: '7.5.1' };
|
||||
export const packageInfo = { name: '@polkadot/wasm-crypto-asmjs', path: 'auto', type: 'auto', version: '7.5.4' };
|
||||
|
||||
@@ -18,20 +18,20 @@
|
||||
"./packageDetect.cjs"
|
||||
],
|
||||
"type": "module",
|
||||
"version": "7.5.1",
|
||||
"version": "7.5.4",
|
||||
"browser": "wasm.js",
|
||||
"main": "wasm.js",
|
||||
"react-native": "asm.js",
|
||||
"dependencies": {
|
||||
"@polkadot/wasm-bridge": "7.5.1",
|
||||
"@polkadot/wasm-crypto-asmjs": "7.5.1",
|
||||
"@polkadot/wasm-crypto-wasm": "7.5.1",
|
||||
"@polkadot/wasm-util": "7.5.1",
|
||||
"@polkadot/wasm-bridge": "7.5.4",
|
||||
"@polkadot/wasm-crypto-asmjs": "7.5.4",
|
||||
"@polkadot/wasm-crypto-wasm": "7.5.4",
|
||||
"@polkadot/wasm-util": "7.5.4",
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@polkadot/util": "^13.5.5",
|
||||
"@polkadot/x-randomvalues": "^13.5.5"
|
||||
"@polkadot/util": "^14.0.1",
|
||||
"@polkadot/x-randomvalues": "^14.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "*",
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-init authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-init authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { InitFn } from '@polkadot/wasm-bridge/types';
|
||||
import type { WasmCryptoInstance } from './types.js';
|
||||
|
||||
import { createWasmFn } from '@polkadot/wasm-bridge';
|
||||
import * as mod from '@polkadot/wasm-crypto-asmjs';
|
||||
|
||||
const { asmJsInit } = mod;
|
||||
import { asmJsInit } from '@polkadot/wasm-crypto-asmjs';
|
||||
|
||||
export { packageInfo } from './packageInfo.js';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-init authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-init authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { InitFn } from '@polkadot/wasm-bridge/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-init authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-init authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export * from './wasm.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-init authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-init authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export * from './wasm.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-init authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-init authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { InitFn } from '@polkadot/wasm-bridge/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2017-2025 @polkadot/wasm-crypto-init authors & contributors
|
||||
// Copyright 2017-2026 @polkadot/wasm-crypto-init authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Copyright 2017-2025 @polkadot/wasm-crypto-init authors & contributors
|
||||
// Copyright 2017-2026 @polkadot/wasm-crypto-init authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Do not edit, auto-generated by @polkadot/dev
|
||||
|
||||
export const packageInfo = { name: '@polkadot/wasm-crypto-init', path: 'auto', type: 'auto', version: '7.5.1' };
|
||||
export const packageInfo = { name: '@polkadot/wasm-crypto-init', path: 'auto', type: 'auto', version: '7.5.4' };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-init authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-init authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { WasmBaseInstance } from '@polkadot/wasm-bridge/types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-init authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-init authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { InitFn } from '@polkadot/wasm-bridge/types';
|
||||
|
||||
@@ -18,14 +18,14 @@
|
||||
"./packageDetect.cjs"
|
||||
],
|
||||
"type": "module",
|
||||
"version": "7.5.1",
|
||||
"version": "7.5.4",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@polkadot/wasm-util": "7.5.1",
|
||||
"@polkadot/wasm-util": "7.5.4",
|
||||
"tslib": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@polkadot/util": "^13.5.5"
|
||||
"@polkadot/util": "^14.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@polkadot/util": "*"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { base64Decode, unzlibSync } from '@polkadot/wasm-util';
|
||||
|
||||
2
packages/wasm-crypto-wasm/src/cjs/bytes.d.ts
vendored
2
packages/wasm-crypto-wasm/src/cjs/bytes.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export declare const bytes: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
exports.lenIn = 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
const bytes = require('../bytes.js');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export declare const bytes: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export const bytes = '';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2025 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// Copyright 2019-2026 @polkadot/wasm-crypto-wasm authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import './packageDetect.js';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user