diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0be9712f..38ffec46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,7 @@ jobs: - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # nightly with: toolchain: nightly + components: rustfmt - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 @@ -83,12 +84,22 @@ jobs: with: node-version: 22 - - name: Install workspace deps - run: npm ci --ignore-scripts - + # codegen.sh emits the gitignored generated TS first and only then + # installs/builds the npm workspace, so package `prepare` (tsc -b) + # scripts see the generated modules. Installing the workspace before + # codegen fails: npm runs the truapi-host-wasm prepare build against + # sources whose ./generated imports don't exist yet. - name: Run codegen run: ./scripts/codegen.sh + - name: Check committed Rust codegen output is current + run: git diff --exit-code -- rust/crates/truapi-server/src/generated + + - name: Wire-table parity (Rust vs generated TS) + run: cargo test -p truapi-server --test wire_table_ts_parity + env: + TRUAPI_REQUIRE_GENERATED_TS: 1 + - name: Upload codegen output uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -101,6 +112,7 @@ jobs: js/packages/truapi/src/explorer/codegen js/packages/truapi/src/explorer/versions.ts js/packages/truapi-host/src/generated + js/packages/truapi-host-wasm/src/generated playground/test/generated ts-client: @@ -219,60 +231,86 @@ jobs: run: npm run build e2e: - name: E2E (playground inside dotli) - # Temporarily disabled while the dotli playground smoke test is stabilized. - if: false + name: E2E (dotli playground diagnosis) runs-on: ubuntu-latest needs: playground - timeout-minutes: 30 + timeout-minutes: 35 env: TRUAPI_REQUIRE_GENERATED: 1 + SIGNER_BOT_SVC_TOKEN: ${{ secrets.SIGNER_BOT_SVC_TOKEN }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - submodules: recursive persist-credentials: false + - name: Check dotli submodule access + id: dotli-access + env: + DOTLI_CHECKOUT_TOKEN: ${{ secrets.DOTLI_CHECKOUT_TOKEN }} + run: | + if [ -n "$DOTLI_CHECKOUT_TOKEN" ]; then + git config --global url."https://x-access-token:${DOTLI_CHECKOUT_TOKEN}@github.com/".insteadOf "https://github.com/" + fi + if git ls-remote https://github.com/paritytech/dotli.git >/dev/null 2>&1; then + echo "available=true" >> "$GITHUB_OUTPUT" + git submodule update --init --recursive hosts/dotli + else + echo "::warning::dotli submodule is not accessible to this workflow; skipping dotli e2e." + echo "available=false" >> "$GITHUB_OUTPUT" + fi + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + if: steps.dotli-access.outputs.available == 'true' with: node-version: 22 - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + if: steps.dotli-access.outputs.available == 'true' with: bun-version: latest + - uses: dtolnay/rust-toolchain@5b842231ba77f5c045dba54ac5560fed2db780e2 # stable + if: steps.dotli-access.outputs.available == 'true' + with: + toolchain: stable + - name: Download codegen output + if: steps.dotli-access.outputs.available == 'true' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: codegen-output - - name: Build @parity/truapi - run: | - npm ci --ignore-scripts - npm run build --prefix js/packages/truapi + - name: Install wasm-pack + if: steps.dotli-access.outputs.available == 'true' + run: cargo install wasm-pack --version 0.14.0 --locked - name: Install dotli deps + if: steps.dotli-access.outputs.available == 'true' working-directory: hosts/dotli run: bun install --frozen-lockfile - - name: Install playground deps - working-directory: playground - run: yarn install --frozen-lockfile - - name: Install Playwright browsers - working-directory: playground - run: npx playwright install --with-deps chromium + if: steps.dotli-access.outputs.available == 'true' + working-directory: hosts/dotli/apps/host + run: bunx playwright install --with-deps chromium - - name: Run Playwright e2e - working-directory: playground - run: yarn e2e - - - name: Upload Playwright report - if: failure() + - name: Run dotli diagnosis e2e + if: steps.dotli-access.outputs.available == 'true' + run: | + if [ -n "$SIGNER_BOT_SVC_TOKEN" ]; then + make e2e-dotli + else + echo "::warning::SIGNER_BOT_SVC_TOKEN is unavailable; running QR smoke mode only." + E2E_DOTLI_SMOKE=1 make e2e-dotli + fi + + - name: Upload dotli e2e results + if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: playwright-report - path: playground/playwright-report + name: dotli-e2e-results + path: hosts/dotli/test-results/e2e-dotli + if-no-files-found: ignore retention-days: 14 ci-status: diff --git a/.github/workflows/host-packages.yml b/.github/workflows/host-packages.yml new file mode 100644 index 00000000..5d13d1f3 --- /dev/null +++ b/.github/workflows/host-packages.yml @@ -0,0 +1,71 @@ +name: host-packages +on: + push: + branches: [main] + paths: + - 'js/packages/truapi-host-wasm/**' + - 'rust/crates/truapi-server/**' + - 'rust/crates/truapi-platform/**' + - 'rust/crates/truapi-codegen/**' + - 'rust/crates/truapi-macros/**' + - 'rust/crates/truapi/**' + pull_request: + paths: + - 'js/packages/truapi-host-wasm/**' + - 'rust/crates/truapi-server/**' + - 'rust/crates/truapi-platform/**' + - 'rust/crates/truapi-codegen/**' + - 'rust/crates/truapi-macros/**' + - 'rust/crates/truapi/**' + +jobs: + wasm-bundle-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: actions/setup-node@v4 + with: { node-version: 22 } + - name: Install wasm-pack + run: cargo install wasm-pack + # Builds the bundle from the current truapi-server source and runs the + # truapi-host-wasm node smoke test against those freshly-built artifacts. + # The dist/wasm/ artifacts are gitignored; every consumer (local dev via + # `make wasm`, CI here) builds them from the Rust source. + - name: Build WASM + run: make wasm + - name: Sanity-check artifact presence + run: | + test -s js/packages/truapi-host-wasm/dist/wasm/web/truapi_server_bg.wasm + test -s js/packages/truapi-host-wasm/dist/wasm/web/truapi_server.js + test -s js/packages/truapi-host-wasm/dist/wasm/node/truapi_server_bg.wasm + test -s js/packages/truapi-host-wasm/dist/wasm/node/truapi_server.js + - name: Build @parity/truapi-host (dep) + run: cd js/packages/truapi-host && npm install --no-fund --no-audit && npm run build + - name: Build @parity/truapi client (dep) + run: cd js/packages/truapi && npm install --no-fund --no-audit && npm run build + - name: Test @parity/truapi-host-wasm against the rebuilt bundle + run: cd js/packages/truapi-host-wasm && npm install --no-fund --no-audit && npm test + host-packages-js-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: actions/setup-node@v4 + with: { node-version: 22 } + - name: Install wasm-pack + run: cargo install wasm-pack + # The truapi-host-wasm tests load dist/wasm/{web,node}, which is + # gitignored, so build it from the Rust source first. + - name: Build WASM + run: make wasm + - name: Build @parity/truapi-host (dep) + run: cd js/packages/truapi-host && npm install --no-fund --no-audit && npm run build + - name: Build @parity/truapi client (dep) + run: cd js/packages/truapi && npm install --no-fund --no-audit && npm run build + - name: Test @parity/truapi-host-wasm + run: cd js/packages/truapi-host-wasm && npm install --no-fund --no-audit && npm test diff --git a/.gitignore b/.gitignore index 9138e1a8..620568d2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,12 @@ lerna-debug.log* node_modules target +# Gradle (Android workspace at repo root) +/.gradle/ +/build/ +/android/*/build/ +local.properties + # Environment / secrets (never commit real env files; keep example templates) .env .env.* @@ -39,3 +45,16 @@ playground/public/static.files # Auto-generated by truapi-codegen (typecheck fixtures for rustdoc ts blocks) playground/test/generated/ + +# Auto-generated FFI / WASM binding outputs +android/truapi-host/src/main/kotlin/generated/ +ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift +ios/truapi-host/Sources/truapi_serverFFI/ +rust/crates/truapi-server/pkg/ +js/packages/truapi/src/generated/ +js/packages/truapi/dist/generated/ +js/packages/truapi-host/src/generated/ +js/packages/truapi-host/dist/generated/ +js/packages/truapi-host-wasm/src/generated/ +js/packages/truapi-host-wasm/dist/generated/ +js/packages/truapi-host-wasm/dist/wasm/ diff --git a/Cargo.lock b/Cargo.lock index 7bb9e809..40f8c40e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,68 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[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 = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "anstream" version = "1.0.0" @@ -38,7 +100,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -49,7 +111,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -59,202 +121,198 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] -name = "arrayvec" -version = "0.7.6" +name = "arbitrary" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" [[package]] -name = "bitvec" -version = "1.0.1" +name = "arrayref" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] -name = "byte-slice-cast" -version = "1.2.3" +name = "arrayvec" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] [[package]] -name = "clap" -version = "4.6.1" +name = "arrayvec" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" -dependencies = [ - "clap_builder", - "clap_derive", -] +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] -name = "clap_builder" -version = "4.6.0" +name = "askama" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", ] [[package]] -name = "clap_derive" -version = "4.6.1" +name = "askama_derive" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" dependencies = [ - "heck", + "askama_parser", + "basic-toml", + "memchr", "proc-macro2", "quote", + "rustc-hash", + "serde", + "serde_derive", "syn", ] [[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "const_format" -version = "0.2.36" +name = "askama_parser" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" dependencies = [ - "const_format_proc_macros", - "konst", + "memchr", + "serde", + "serde_derive", + "winnow 0.7.15", ] [[package]] -name = "const_format_proc_macros" -version = "0.2.34" +name = "async-channel" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", ] [[package]] -name = "convert_case" -version = "0.6.0" +name = "async-executor" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ - "unicode-segmentation", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", ] [[package]] -name = "convert_case" -version = "0.10.0" +name = "async-fs" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" dependencies = [ - "unicode-segmentation", + "async-lock", + "blocking", + "futures-lite", ] [[package]] -name = "derive_more" -version = "2.1.1" +name = "async-io" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ - "derive_more-impl", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", ] [[package]] -name = "derive_more-impl" -version = "2.1.1" +name = "async-lock" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ - "convert_case 0.10.0", - "proc-macro2", - "quote", - "rustc_version", - "syn", - "unicode-xid", + "event-listener", + "event-listener-strategy", + "pin-project-lite", ] [[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "funty" +name = "async-net" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "async-io", + "blocking", + "futures-lite", ] [[package]] -name = "futures-channel" -version = "0.3.32" +name = "async-process" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ - "futures-core", - "futures-sink", + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", ] [[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" +name = "async-signal" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", "futures-core", - "futures-task", - "futures-util", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", ] [[package]] -name = "futures-io" -version = "0.3.32" +name = "async-task" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] -name = "futures-macro" -version = "0.3.32" +name = "async-trait" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -262,399 +320,4107 @@ dependencies = [ ] [[package]] -name = "futures-sink" -version = "0.3.32" +name = "atomic-take" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "a8ab6b55fe97976e46f91ddbed8d147d966475dc29b2032757ba47e02376fbc3" [[package]] -name = "futures-task" -version = "0.3.32" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] -name = "futures-util" -version = "0.3.32" +name = "autocfg" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "hashbrown" -version = "0.17.0" +name = "base16ct" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] -name = "heck" -version = "0.5.0" +name = "base32" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" [[package]] -name = "hex" -version = "0.4.3" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "impl-trait-for-tuples" -version = "0.2.3" +name = "base64ct" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" dependencies = [ - "proc-macro2", - "quote", - "syn", + "serde", ] [[package]] -name = "indexmap" -version = "2.14.0" +name = "bip39" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ - "equivalent", - "hashbrown", + "bitcoin_hashes", ] [[package]] -name = "indoc" -version = "2.0.7" +name = "bitcoin_hashes" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ - "rustversion", + "hex-conservative", ] [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "bitflags" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] -name = "itoa" -version = "1.0.18" +name = "bitvec" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] [[package]] -name = "konst" -version = "0.2.20" +name = "blake2-rfc" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" dependencies = [ - "konst_macro_rules", + "arrayvec 0.4.12", + "constant_time_eq 0.1.5", ] [[package]] -name = "konst_macro_rules" -version = "0.2.19" +name = "blake2b_simd" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "constant_time_eq 0.4.2", +] [[package]] -name = "memchr" -version = "2.8.0" +name = "block-buffer" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] [[package]] -name = "once_cell_polyfill" -version = "1.70.2" +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[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 = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f81cede359311706057b689b91b59f464926de0316f389898a2b028cb494fa" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa6ca11305de425ea08884097b913ebe1a83875253b3c0063ce28411e226bfdc" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7537341a9a4ba9812141927be733e7254bf2318aab6597d567af9cad90609f27" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d28a4ca5faf25ff821fcc768f26e68ffef505e9f71bb06e608862d941fa65086" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d891057fe1b73910c41e73b32a70fa8454092fce65942b5fa6f72aa6d5487f8a" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.15.5", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-math", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c29a66028a78eedc534b3a94e5ebfbaeb4e1f6b09038afe41bb24afd614faa4b" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95809ad251fe9422087b4a72d61e584d6ab6eff44dee1335f93cfaea0bedc9ac" + +[[package]] +name = "cranelift-control" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79d0cacf063c297e5e8d5b73cb355b41b87f6d248e252d1b284e7a7b73673c2" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d73297a195ce3be55997c6307142c4b1e58dd0c2f18ceaa0179444024e312a" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be38d1ae29ef7c5d611fc6cb694f698dc4ca44152dcaa112ec0fef8d4d34858" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6761926f6636209de7ac568be28b206890f2181761375b9722e0a1e7a7e1637a" + +[[package]] +name = "cranelift-native" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0893472f73f0d530a28e9a573ada6d1f93b9659bb6734dfe17061ac967bd1830" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.123.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1daccebabb1ccd034dbab0eacc0722af27d3cccc7929dea27a3546cb3562e40" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case 0.10.0", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-zebra" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775765289f7c6336c18d3d66127527820dd45ffd9eb3b6b8ee4708590e6c20f5" +dependencies = [ + "curve25519-dalek", + "ed25519", + "hashbrown 0.16.1", + "pkcs8", + "rand_core", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastbloom" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8" +dependencies = [ + "foldhash 0.2.0", + "libm", + "portable-atomic", + "siphasher 1.0.3", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +dependencies = [ + "gloo-timers", + "send_wrapper 0.4.0", +] + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "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", + "zeroize", +] + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "getrandom_or_panic" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" +dependencies = [ + "rand_core", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +dependencies = [ + "fallible-iterator", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec 0.7.6", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[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", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +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.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[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 = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libsecp256k1" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" +dependencies = [ + "arrayref", + "base64", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core", + "zeroize", +] + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multi-stash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685a9ac4b61f4e728e1d2c6a7844609c16527aeb5e6c865915c08e619c16410f" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "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", + "libm", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "elliptic-curve", + "primeorder", +] [[package]] name = "parity-scale-codec" version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec 0.7.6", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[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", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[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.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulley-interpreter" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b78fdec962b639b921badfcfe77db7d18aa3c0c1e292ac2aa268c0efe8fe683" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-math", +] + +[[package]] +name = "pulley-macros" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f718f4e8cd5fdfa08b3b1d2d25fe288350051be330544305f0a9b93a937b3d42" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +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 = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[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", +] + +[[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", +] + +[[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 = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regalloc2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ruzstd" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schnorrkel" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9fcb6c2e176e86ec703e22560d99d65a5ee9056ae45a08e13e84ebf796296f" +dependencies = [ + "aead", + "arrayref", + "arrayvec 0.7.6", + "curve25519-dalek", + "getrandom_or_panic", + "merlin", + "rand_core", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zeroize", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + +[[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_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[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", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "smol" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33bd3e260892199c3ccfc487c88b2da2265080acb316cd920da72fdfd7c599f" +dependencies = [ + "async-channel", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-net", + "async-process", + "blocking", + "futures-lite", +] + +[[package]] +name = "smoldot" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b22238c3655a9e66285b5fc1a59aa914a3c09160caf5b9604f8c11de787d77f" +dependencies = [ + "arrayvec 0.7.6", + "async-lock", + "atomic-take", + "base32", + "base64", + "bip39", + "blake2-rfc", + "bs58", + "chacha20", + "crossbeam-queue", + "derive_more", + "ed25519-zebra", + "either", + "event-listener", + "fastbloom", + "fnv", + "futures-lite", + "futures-util", + "hashbrown 0.16.1", + "hex", + "hmac 0.12.1", + "itertools", + "libm", + "libsecp256k1", + "merlin", + "nom 8.0.0", + "num-bigint", + "num-rational", + "num-traits", + "parking_lot", + "pbkdf2", + "pin-project", + "poly1305", + "rand", + "rand_chacha", + "rusqlite", + "ruzstd", + "schnorrkel", + "serde", + "serde_json", + "sha2 0.10.9", + "sha3", + "siphasher 1.0.3", + "slab", + "smallvec", + "soketto", + "twox-hash 2.1.2", + "wasmi", + "wasmtime", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "smoldot-light" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ec52700b57df3f2a031873c5de7d44f2f8abae54b622ccf15ad9bc7c05a7c2" +dependencies = [ + "async-channel", + "async-lock", + "base64", + "blake2-rfc", + "bs58", + "derive_more", + "either", + "event-listener", + "fnv", + "futures-channel", + "futures-lite", + "futures-util", + "hashbrown 0.16.1", + "hex", + "itertools", + "log", + "lru", + "parking_lot", + "pin-project", + "rand", + "rand_chacha", + "serde", + "serde_json", + "siphasher 1.0.3", + "slab", + "smol", + "smoldot", + "zeroize", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "soketto" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" +dependencies = [ + "base64", + "bytes", + "futures", + "httparse", + "log", + "rand", + "sha1", +] + +[[package]] +name = "sp-crypto-hashing" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9927a7f81334ed5b8a98a4a978c81324d12bd9713ec76b5c68fd410174c5eb" +dependencies = [ + "blake2b_simd", + "byteorder", + "digest 0.10.7", + "sha2 0.10.9", + "sha3", + "twox-hash 1.6.3", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[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.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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 = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[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.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "truapi" +version = "0.3.0" +dependencies = [ + "derive_more", + "futures", + "hex", + "parity-scale-codec", + "truapi-macros", +] + +[[package]] +name = "truapi-codegen" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "convert_case 0.6.0", + "indoc", + "serde", + "serde_json", + "tempfile", + "truapi", +] + +[[package]] +name = "truapi-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "truapi-platform" +version = "0.1.0" +dependencies = [ + "futures", + "truapi", + "url", +] + +[[package]] +name = "truapi-server" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "async-trait", + "blake2-rfc", + "bs58", + "console_error_panic_hook", + "derive_more", + "futures", + "futures-timer", + "futures-util", + "getrandom 0.2.17", + "hex", + "hkdf", + "js-sys", + "p256", + "parity-scale-codec", + "pin-project", + "rand", + "schnorrkel", + "send_wrapper 0.6.0", + "serde_json", + "sha2 0.10.9", + "smoldot", + "smoldot-light", + "sp-crypto-hashing", + "thiserror 1.0.69", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", + "truapi", + "truapi-platform", + "unicode-normalization", + "uniffi", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test", + "web-sys", + "web-time", +] + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror 1.0.69", + "url", + "utf-8", +] + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "digest 0.10.7", + "static_assertions", +] + +[[package]] +name = "twox-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[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 = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uniffi" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f" +dependencies = [ + "anyhow", + "cargo_metadata", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "indexmap", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_core" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" +dependencies = [ + "anyhow", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933" +dependencies = [ + "anyhow", + "indexmap", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "uniffi_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn", + "toml", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" dependencies = [ - "arrayvec", - "bitvec", - "byte-slice-cast", - "const_format", - "impl-trait-for-tuples", - "parity-scale-codec-derive", + "anyhow", + "siphasher 0.3.11", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af5ec93229ad9ccd0a545a516dec76dc276613f278f6a91aa6b463d5b33d42d0" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", ] [[package]] -name = "parity-scale-codec-derive" -version = "3.7.5" +name = "wasm-bindgen-test-macro" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c81b9fef827e575e0e54431736d1baa0d700315d8c62cfef1f61fa3aad0cbeb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4d8ae7ad5440360e9799dfd42857d126454a88441ddf72d288ef83fa47f527" + +[[package]] +name = "wasm-encoder" +version = "0.236.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "724fccfd4f3c24b7e589d333fc0429c68042897a7e8a5f8694f31792471841e7" +dependencies = [ + "leb128fmt", + "wasmparser 0.236.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", +] + +[[package]] +name = "wasmi" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19af97fcb96045dd1d6b4d23e2b4abdbbe81723dbc5c9f016eb52145b320063" +dependencies = [ + "arrayvec 0.7.6", + "multi-stash", + "smallvec", + "spin", + "wasmi_collections", + "wasmi_core", + "wasmi_ir", + "wasmparser 0.221.3", +] + +[[package]] +name = "wasmi_collections" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e80d6b275b1c922021939d561574bf376613493ae2b61c6963b15db0e8813562" + +[[package]] +name = "wasmi_core" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8c51482cc32d31c2c7ff211cd2bedd73c5bd057ba16a2ed0110e7a96097c33" +dependencies = [ + "downcast-rs", + "libm", +] + +[[package]] +name = "wasmi_ir" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e431a14c186db59212a88516788bd68ed51f87aa1e08d1df742522867b5289a" +dependencies = [ + "wasmi_core", +] + +[[package]] +name = "wasmparser" +version = "0.221.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" +dependencies = [ + "bitflags", +] + +[[package]] +name = "wasmparser" +version = "0.236.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b1e81f3eb254cf7404a82cee6926a4a3ccc5aad80cc3d43608a070c67aa1d7" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.236.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2df225df06a6df15b46e3f73ca066ff92c2e023670969f7d50ce7d5e695abbb1" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.236.1", +] + +[[package]] +name = "wasmtime" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10306ead921db2c4645ff99867b7539b65e18afd8816d471547f5e6f3b09492" +dependencies = [ + "addr2line", + "anyhow", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "hashbrown 0.15.5", + "indexmap", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rustix", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-asm-macros", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-math", + "wasmtime-internal-slab", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-environ" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7fb2c37ca263d444f33871bf0221e7de0707b2b2bb88165df6db6d58c73375f" +dependencies = [ + "anyhow", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "indexmap", + "log", + "object", + "postcard", + "semver", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.236.1", + "wasmparser 0.236.1", + "wasmprinter", + "wasmtime-internal-component-util", +] + +[[package]] +name = "wasmtime-internal-asm-macros" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19c6c0d3c8d2db554a3af8e8d413ff2815362ebce0911808ecfdaaa257438f93" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e3f3752466eb0e1f97149e53bf15c0e18ff520fc0a98b4bee1680e6de1c6f0" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser 0.236.1", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f54018baf62f4e9c616c31f2aeadcf0c202ff691a390ad53e291ae7160b169e" + +[[package]] +name = "wasmtime-internal-cranelift" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a2412f2afb0a5db2a4ac1cfff73247e240aeaa90bf41497ad0a5084b6a24eca" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-math", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecfdc460dd5d343d88ff1ffaf65ae019feeb6124ddcfd3f39d28331068d25b1f" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "libc", + "rustix", + "wasmtime-internal-asm-macros", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5abb428a71827b7f90fc64406749883ccc6e58addf6d36974d5e06942011707" +dependencies = [ + "cc", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6cc13f14c3fb83fb877cb1d5c605e93f7ec1bf7fc1a5e8b361209d2f8ca028" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-internal-math" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cb209473a09f4dbd9c87bb9f18b8dcb0c9da30d12a260e3eacf7a1a53b41480" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-internal-slab" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aab4df5a04752106e1ecef9d40145ef28fa033b0d5dd3c839c9b208b2d522183" + +[[package]] +name = "wasmtime-internal-unwinder" +version = "36.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5359875d29bddb6f7e65e698157714d8d35ebd8ea2a92893d05d6b062147b639" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "log", + "object", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "36.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +checksum = "2e247bcdd69701743ba386c933b26ebad2ce912ff9cb68b5b71fdb29d39ba04a" dependencies = [ - "proc-macro-crate", "proc-macro2", "quote", "syn", ] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "wasmtime-internal-winch" +version = "36.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "d0298dfd9f57588222b5a92dcffe75894f1ead4e519850f176bde7fcfd105d54" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli", + "object", + "target-lexicon", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] [[package]] -name = "proc-macro-crate" -version = "3.5.0" +name = "wasmtime-internal-wit-bindgen" +version = "36.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +checksum = "1706803e83b9bae726a0f55e7c1bbf78a7421cf2da68c940c70978e91dfc0339" dependencies = [ - "toml_edit", + "anyhow", + "bitflags", + "heck", + "indexmap", + "wit-parser 0.236.1", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "web-sys" +version = "0.3.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" dependencies = [ - "unicode-ident", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "quote" -version = "1.0.45" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ - "proc-macro2", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "radium" -version = "0.7.0" +name = "weedle2" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom 7.1.3", +] [[package]] -name = "rustc_version" -version = "0.4.1" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "semver", + "windows-sys 0.61.2", ] [[package]] -name = "rustversion" -version = "1.0.22" +name = "winch-codegen" +version = "36.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "2e2d7ea2137be52644d9c42ca5a4899bba07c2ed2db1e66c4c1994adfe35d39e" +dependencies = [ + "anyhow", + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "wasmtime-internal-math", +] [[package]] -name = "semver" -version = "1.0.28" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "serde" -version = "1.0.228" +name = "windows-sys" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "serde_core", - "serde_derive", + "windows-targets", ] [[package]] -name = "serde_core" -version = "1.0.228" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "serde_derive", + "windows-link", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "windows-targets" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "serde_json" -version = "1.0.149" +name = "windows_aarch64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ - "itoa", "memchr", - "serde", - "serde_core", - "zmij", ] [[package]] -name = "slab" -version = "0.4.12" +name = "winnow" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] [[package]] -name = "strsim" -version = "0.11.1" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] [[package]] -name = "syn" -version = "2.0.117" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser 0.244.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ + "anyhow", + "prettyplease", "proc-macro2", "quote", - "unicode-ident", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] -name = "tap" -version = "1.0.1" +name = "wit-component" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder 0.244.0", + "wasm-metadata", + "wasmparser 0.244.0", + "wit-parser 0.244.0", +] [[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +name = "wit-parser" +version = "0.236.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "16e4833a20cd6e85d6abfea0e63a399472d6f88c6262957c17f546879a80ba15" dependencies = [ - "serde_core", + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.236.1", ] [[package]] -name = "toml_edit" -version = "0.25.11+spec-1.1.0" +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ + "anyhow", + "id-arena", "indexmap", - "toml_datetime", - "toml_parser", - "winnow", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.244.0", ] [[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" +name = "writeable" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ - "winnow", + "tap", ] [[package]] -name = "truapi" -version = "0.3.0" +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ - "derive_more", - "futures", - "hex", - "parity-scale-codec", - "truapi-macros", + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", ] [[package]] -name = "truapi-codegen" -version = "0.1.0" +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ - "anyhow", - "clap", - "convert_case 0.6.0", - "indoc", - "serde", - "serde_json", - "truapi", + "stable_deref_trait", + "yoke-derive", + "zerofrom", ] [[package]] -name = "truapi-macros" -version = "0.1.0" +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", "syn", + "synstructure", ] [[package]] -name = "unicode-ident" -version = "1.0.24" +name = "zerocopy" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] [[package]] -name = "unicode-segmentation" -version = "1.13.2" +name = "zerocopy-derive" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "unicode-xid" -version = "0.2.6" +name = "zerofrom" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] [[package]] -name = "utf8parse" -version = "0.2.2" +name = "zerofrom-derive" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] -name = "windows-link" -version = "0.2.1" +name = "zeroize" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] [[package]] -name = "windows-sys" -version = "0.61.2" +name = "zeroize_derive" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ - "windows-link", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "winnow" -version = "1.0.2" +name = "zerotrie" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ - "memchr", + "displaydoc", + "yoke", + "zerofrom", ] [[package]] -name = "wyz" -version = "0.5.1" +name = "zerovec" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ - "tap", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/Makefile b/Makefile index c8bc371c..a24ed9df 100644 --- a/Makefile +++ b/Makefile @@ -3,12 +3,23 @@ # Run `make help` for the list of targets. .DEFAULT_GOAL := help -.PHONY: help setup build codegen test check playground dev matrix explorer +.PHONY: help setup build codegen test check playground wasm wasm-crypto-test uniffi android-publish-local dev dev-bootstrap dev-link-check e2e-dotli matrix explorer TRUAPI_PKG := js/packages/truapi PLAYGROUND := playground +JS_PACKAGES := js/packages EXPLORER := explorer DOTLI := hosts/dotli +HOST_WASM_PKG := $(JS_PACKAGES)/truapi-host-wasm +HOST_WASM_GENERATED := $(HOST_WASM_PKG)/src/generated/host-callbacks.ts +HOST_WASM_WEB := $(HOST_WASM_PKG)/dist/wasm/web/truapi_server.js +HOST_WASM_NODE := $(HOST_WASM_PKG)/dist/wasm/node/truapi_server.js +DOTLI_UI := $(DOTLI)/packages/ui +DOTLI_HOST_WASM_LINK := $(DOTLI_UI)/node_modules/@parity/truapi-host-wasm +SIGNER_BOT_BASE_URL ?= https://signing-bot-dev.novasama-tech.org/ +SIGNER_BOT_NETWORK ?= paseo-next-v2 +export SIGNER_BOT_BASE_URL +export SIGNER_BOT_NETWORK # `make dev DEBUG=1` runs dotli with VITE_APP_DEBUG=true to log every wire frame. DOTLI_PREVIEW := preview @@ -18,31 +29,77 @@ endif help: ## Show this help. @awk 'BEGIN { FS = ":.*##"; printf "Usage: make \n\nTargets:\n" } \ - /^[a-zA-Z_-]+:.*?##/ { printf " %-12s %s\n", $$1, $$2 }' $(MAKEFILE_LIST) + /^[a-zA-Z0-9_-]+:.*?##/ { printf " %-12s %s\n", $$1, $$2 }' $(MAKEFILE_LIST) -setup: ## First-time setup: submodules + JS dependencies. +setup: ## First-time setup: submodules, JS dependencies, generated artifacts. git submodule update --init --recursive - cd $(TRUAPI_PKG) && npm install + # --ignore-scripts: the workspace `prepare` builds need generated sources + # that only exist after codegen.sh, which also builds the packages. + npm ci --ignore-scripts + ./scripts/codegen.sh cd $(PLAYGROUND) && yarn install --frozen-lockfile + cd $(DOTLI) && bun install --frozen-lockfile build: ## Build the Rust workspace and the TypeScript client. cargo build --workspace cd $(TRUAPI_PKG) && npm run build + cd $(HOST_WASM_PKG) && npm run build -codegen: ## Regenerate the TypeScript client from the Rust crate. +codegen: ## Regenerate generated TS/Rust artifacts from the Rust crates. ./scripts/codegen.sh cd $(PLAYGROUND) && rm -rf node_modules/@parity && yarn install +wasm: ## Rebuild the truapi-server WASM artifacts under js/packages/truapi-host-wasm/dist/wasm/. + cd $(HOST_WASM_PKG) && npm run build:wasm + +wasm-crypto-test: ## Run crypto/vector tests on wasm32 via wasm-pack/node. + wasm-pack test --node rust/crates/truapi-server --test wasm_crypto_vectors --no-default-features + +UNIFFI_CDYLIB_DIR := target/release +UNAME_S := $(shell uname -s) +ifeq ($(UNAME_S),Darwin) +UNIFFI_CDYLIB := $(UNIFFI_CDYLIB_DIR)/libtruapi_server.dylib +else +UNIFFI_CDYLIB := $(UNIFFI_CDYLIB_DIR)/libtruapi_server.so +endif + +UNIFFI_SWIFT_TMP := target/uniffi-swift-out + +uniffi: ## Regenerate Kotlin + Swift bindings from truapi-server cdylib. + cargo build -p truapi-server --release --features ws-bridge + cargo run -p uniffi-bindgen-cli -- generate \ + --library $(UNIFFI_CDYLIB) \ + --language kotlin \ + --out-dir android/truapi-host/src/main/kotlin/generated + rm -rf $(UNIFFI_SWIFT_TMP) + mkdir -p $(UNIFFI_SWIFT_TMP) + cargo run -p uniffi-bindgen-cli -- generate \ + --library $(UNIFFI_CDYLIB) \ + --language swift \ + --out-dir $(UNIFFI_SWIFT_TMP) + mkdir -p ios/truapi-host/Sources/truapi_serverFFI/include + cp $(UNIFFI_SWIFT_TMP)/truapi_server.swift \ + ios/truapi-host/Sources/TrUAPIHost/truapi_server.swift + cp $(UNIFFI_SWIFT_TMP)/truapi_serverFFI.h \ + ios/truapi-host/Sources/truapi_serverFFI/include/truapi_serverFFI.h + cp $(UNIFFI_SWIFT_TMP)/truapi_serverFFI.modulemap \ + ios/truapi-host/Sources/truapi_serverFFI/include/module.modulemap + +android-publish-local: uniffi ## Publish io.parity:truapi-host-android to ~/.m2 (dev workflow). + gradle :truapi-host:publishReleasePublicationToMavenLocal --no-daemon + test: ## Run Rust + TypeScript client tests. - cargo test --workspace + cargo test --workspace --features ws-bridge cd $(TRUAPI_PKG) && npm test + cd $(JS_PACKAGES)/truapi-host-wasm && npm test check: ## Full verification suite (build, fmt, clippy, test, TS tests, playground build + lint). cargo build --workspace cargo +nightly fmt --check cargo clippy --workspace --all-targets --all-features -- -D warnings - cargo test --workspace + cargo test --workspace --features ws-bridge cd $(TRUAPI_PKG) && npm run build && npm test + cd $(JS_PACKAGES)/truapi-host-wasm && npm install --no-fund --no-audit && npm test cd $(PLAYGROUND) && yarn build && yarn lint playground: ## Refresh the playground's @parity/truapi snapshot and rebuild. @@ -50,12 +107,45 @@ playground: ## Refresh the playground's @parity/truapi snapshot and rebuild. cd $(PLAYGROUND) && rm -rf node_modules/@parity && yarn install cd $(PLAYGROUND) && yarn build -dev: ## Start dotli host (:5173) + playground (:3000) together; open http://localhost:5173/localhost:3000. DEBUG=1 logs wire frames. +dev-bootstrap: ## Prepare ignored generated/build artifacts needed by dotli preview. + git submodule update --init --recursive + # --ignore-scripts: the workspace `prepare` builds need generated sources + # that only exist after codegen.sh, which also builds the packages. + if [ ! -d node_modules ]; then npm ci --ignore-scripts; fi + if [ ! -f "$(HOST_WASM_GENERATED)" ]; then ./scripts/codegen.sh; fi + cd $(HOST_WASM_PKG) && npm run build + TRUAPI_WASM_PROFILE=dev $(MAKE) wasm + cd $(PLAYGROUND) && yarn install --frozen-lockfile + cd $(DOTLI) && bun install --frozen-lockfile + $(MAKE) dev-link-check + +dev-link-check: ## Verify dotli can resolve the local @parity/truapi-host-wasm package. + @test -f "$(HOST_WASM_GENERATED)" || (echo "Missing generated host callbacks. Run: make codegen"; exit 1) + @test -f "$(HOST_WASM_PKG)/dist/index.js" || (echo "Missing @parity/truapi-host-wasm dist. Run: npm run build --prefix $(HOST_WASM_PKG)"; exit 1) + @test -f "$(HOST_WASM_WEB)" || (echo "Missing @parity/truapi-host-wasm web WASM glue. Run: make wasm"; exit 1) + @test -e "$(DOTLI_HOST_WASM_LINK)/package.json" || (echo "dotli cannot resolve @parity/truapi-host-wasm. Run top-level: make dev"; exit 1) + cd $(DOTLI_UI) && bun -e 'await import("@parity/truapi-host-wasm"); await import("@parity/truapi-host-wasm/web");' + +dev: dev-bootstrap ## Start dotli host (:5173) + playground (:3000) together; open http://localhost:5173/localhost:3000. DEBUG=1 logs wire frames. @trap 'kill 0' EXIT; \ ( cd $(DOTLI) && bun run $(DOTLI_PREVIEW) ) & \ ( cd $(PLAYGROUND) && yarn dev ) & \ wait +e2e-dotli: ## Fully automated dotli + playground diagnosis e2e. Requires SIGNER_BOT_SVC_TOKEN unless E2E_DOTLI_SMOKE=1. + @SIGNER_BOT_SVC_TOKEN_ENV="$$SIGNER_BOT_SVC_TOKEN"; \ + SIGNER_BOT_BASE_URL_ENV="$$SIGNER_BOT_BASE_URL"; \ + SIGNER_BOT_NETWORK_ENV="$$SIGNER_BOT_NETWORK"; \ + set -a; \ + if [ -f .env ]; then . ./.env; fi; \ + set +a; \ + if [ -n "$$SIGNER_BOT_SVC_TOKEN_ENV" ]; then SIGNER_BOT_SVC_TOKEN="$$SIGNER_BOT_SVC_TOKEN_ENV"; export SIGNER_BOT_SVC_TOKEN; fi; \ + if [ -n "$$SIGNER_BOT_BASE_URL_ENV" ]; then SIGNER_BOT_BASE_URL="$$SIGNER_BOT_BASE_URL_ENV"; export SIGNER_BOT_BASE_URL; fi; \ + if [ -n "$$SIGNER_BOT_NETWORK_ENV" ]; then SIGNER_BOT_NETWORK="$$SIGNER_BOT_NETWORK_ENV"; export SIGNER_BOT_NETWORK; fi; \ + if [ "$$E2E_DOTLI_SMOKE" != "1" ]; then test -n "$$SIGNER_BOT_SVC_TOKEN" || (echo "Missing SIGNER_BOT_SVC_TOKEN. e2e-dotli requires signer-bot; without it a human phone scan is required."; exit 1); fi; \ + $(MAKE) dev-bootstrap; \ + cd $(DOTLI)/apps/host && bun tests/e2e/playground-diagnosis.ts + matrix: ## Regenerate the host compatibility matrix from explorer/diagnosis-reports. cd $(EXPLORER) && npm run generate-matrix diff --git a/deny.toml b/deny.toml index 707c6f77..14156d84 100644 --- a/deny.toml +++ b/deny.toml @@ -5,8 +5,32 @@ allow = [ "MIT", "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "CC0-1.0", "Unicode-3.0", "Unlicense", "Zlib", ] confidence-threshold = 0.8 + +# uniffi is MPL-2.0: file-level weak copyleft, consumed unmodified as a +# dependency, which MPL-2.0 permits without affecting the MIT outbound licence. +# Scoped per crate so MPL-2.0 stays disallowed everywhere else. +# smoldot is GPL-3.0-or-later WITH Classpath-exception-2.0: the Classpath +# exception explicitly permits linking it into independently-licensed works, +# so the GPL does not extend to this crate. Scoped to the two smoldot crates +# so plain GPL stays disallowed everywhere else. +exceptions = [ + { name = "smoldot", allow = ["GPL-3.0-or-later WITH Classpath-exception-2.0"] }, + { name = "smoldot-light", allow = ["GPL-3.0-or-later WITH Classpath-exception-2.0"] }, + { name = "uniffi", allow = ["MPL-2.0"] }, + { name = "uniffi_bindgen", allow = ["MPL-2.0"] }, + { name = "uniffi_core", allow = ["MPL-2.0"] }, + { name = "uniffi_internal_macros", allow = ["MPL-2.0"] }, + { name = "uniffi_macros", allow = ["MPL-2.0"] }, + { name = "uniffi_meta", allow = ["MPL-2.0"] }, + { name = "uniffi_pipeline", allow = ["MPL-2.0"] }, + { name = "uniffi_udl", allow = ["MPL-2.0"] }, +] diff --git a/docs/rfcs/extended-theme-api.md b/docs/rfcs/extended-theme-api.md index accc6290..95649354 100644 --- a/docs/rfcs/extended-theme-api.md +++ b/docs/rfcs/extended-theme-api.md @@ -59,4 +59,3 @@ The old `Theme` enum (`Light | Dark`) is renamed to `ThemeVariant` for clarity. ## Drawbacks - Wire-breaking change: deployed products on the old schema will fail to decode the new payload until updated. - diff --git a/hosts/dotli b/hosts/dotli index 0d5beea4..832fe422 160000 --- a/hosts/dotli +++ b/hosts/dotli @@ -1 +1 @@ -Subproject commit 0d5beea4b20335e10347512d98365419284ef261 +Subproject commit 832fe42271b033dbfdf538e5acf1a0414f9470b2 diff --git a/js/packages/truapi-host-wasm/.gitignore b/js/packages/truapi-host-wasm/.gitignore new file mode 100644 index 00000000..288deac9 --- /dev/null +++ b/js/packages/truapi-host-wasm/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +*.tsbuildinfo +# Ignore compiled TS output (top-level + the web/ and electron/ entry subdirs) +# Generated WASM artifacts under dist/wasm/ are ignored by the repo root. +dist/**/*.js +dist/**/*.d.ts +dist/**/*.js.map +dist/**/*.d.ts.map +dist/generated/ +# Codegen output from truapi-codegen --platform-ts-output. +src/generated/ diff --git a/js/packages/truapi-host-wasm/README.md b/js/packages/truapi-host-wasm/README.md new file mode 100644 index 00000000..d6945ef1 --- /dev/null +++ b/js/packages/truapi-host-wasm/README.md @@ -0,0 +1,117 @@ +# @parity/truapi-host-wasm + +WASM-backed TrUAPI host runtime. It embeds the `truapi-server` Rust core (compiled to WASM) and +provides the `Provider` factories that drive it, plus per-environment integration entry points. +It is the counterpart to the native Android/iOS host shells. + +> This is distinct from [`@parity/truapi-host`](../truapi-host), which is the host-side codegen + +> dispatcher for hosts that bring their **own** runtime and do not embed the shared Rust core. + +## Entry points + +The package exposes tree-shakeable subpath exports — import only what your environment needs: + +| Import | Provides | +| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | +| `@parity/truapi-host-wasm` | Core: `createWasmProvider`, `createNodeWasmProvider`, `createHostServer`, the dispatcher adapter, and the shared types. | +| `@parity/truapi-host-wasm/web` | Browser host: `createIframeHost` (iframe MessageChannel handshake) and `createWebWorkerProvider`. | +| `@parity/truapi-host-wasm/electron` | `createElectronProvider` — wraps an Electron `MessagePortMain` as a `Provider`. | +| `@parity/truapi-host-wasm/worker-runtime` | Web Worker entrypoint (import with your bundler's `?worker` suffix) so the WASM core runs off the page main thread. | +| `@parity/truapi-host-wasm/wasm/{web,node}` | The raw `wasm-bindgen` glue, if you need to instantiate the core yourself. | + +## Generated WASM artefacts + +The ignored bundles under `dist/wasm/web/` and `dist/wasm/node/` are built without smoldot +(`wasm-pack build --no-default-features`). Hosts that already manage chain access through their own +JSON-RPC provider wire `chainConnect` into the callbacks and never touch smoldot. The bundled WASM +is about 1 MB (release build with `wasm-opt`). + +Build them after editing `rust/crates/truapi-server` and before packaging, publishing, or running +tests that load the raw WASM bundle (requires `wasm-pack` on PATH): + +```bash +npm run build:wasm # or `make wasm` from the repo root +``` + +## Example — Node / Electron + +```ts +import { + createNodeWasmProvider, + createHostServer, +} from "@parity/truapi-host-wasm"; +import { createElectronProvider } from "@parity/truapi-host-wasm/electron"; + +const provider = await createNodeWasmProvider( + { + navigateTo: async (url) => { + /* shell.openExternal(url) */ + }, + pushNotification: async () => {}, + devicePermission: async () => true, + remotePermission: async () => true, + featureSupported: async (payload) => payload, + localStorageRead: async () => undefined, + localStorageWrite: async () => {}, + localStorageClear: async () => {}, + // Optional: authStateChanged (core-owned auth UI state stream), + // readSession/writeSession/clearSession, subscribeSessionStore, + // confirmation, preimage, theme, and chain callbacks. + }, + { + runtimeConfig: { + productLabel: "example", + productId: "example.dot", + siteId: "dot.li", + hostName: "Polkadot Web", + hostIcon: "https://dot.li/dotli.png", + hostVersion: "0.5.0", + platformType: "browser", + platformVersion: "unknown", + peopleChainGenesisHash: + "0xa22a2424d2cbf561eaecf7da8b1b548fa9d1939f60265e942b1049616a012f71", + pairingDeeplinkScheme: "polkadotapp", + }, + }, +); + +const server = createHostServer(provider, [ + /* dispatch entries */ +]); +``` + +## Example — browser (Web Worker) + +```ts +import HostWorker from "@parity/truapi-host-wasm/worker-runtime?worker"; +import { createWebWorkerProvider } from "@parity/truapi-host-wasm/web"; + +const provider = await createWebWorkerProvider(new HostWorker(), callbacks, { + runtimeConfig, +}); +``` + +`@parity/truapi-host-wasm/web` also exports `createIframeHost` for the protocol-iframe +MessageChannel handshake. + +## Publishing + +The npm publish workflow is not wired yet. A release-process discussion is needed before adding a +publish job to `.github/workflows/`. Until then, consumers depend on the package via the workspace +`file:` link or by publishing locally with `npm pack`. + +## Architecture + +```text +JS host code + protocol handlers / typed callbacks + | + v +createHostServer (re-exported from @parity/truapi-host) <-- bytes --> Provider + | + v + createWasmProvider / Worker + | + v + truapi-server WASM core +``` diff --git a/js/packages/truapi-host-wasm/package.json b/js/packages/truapi-host-wasm/package.json new file mode 100644 index 00000000..c48e2a70 --- /dev/null +++ b/js/packages/truapi-host-wasm/package.json @@ -0,0 +1,61 @@ +{ + "name": "@parity/truapi-host-wasm", + "version": "0.1.0", + "description": "WASM-backed TrUAPI host runtime: embeds the Rust core, with web (iframe + Web Worker) and Electron (MessagePortMain) entry points", + "license": "MIT", + "author": "Parity Technologies ", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "sideEffects": [ + "./dist/worker-runtime.js", + "./dist/wasm/**" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./web": { + "types": "./dist/web/index.d.ts", + "import": "./dist/web/index.js" + }, + "./electron": { + "types": "./dist/electron/index.d.ts", + "import": "./dist/electron/index.js" + }, + "./node-runtime": { + "types": "./dist/node-runtime.d.ts", + "import": "./dist/node-runtime.js" + }, + "./worker-runtime": { + "types": "./dist/worker-runtime.d.ts", + "import": "./dist/worker-runtime.js" + }, + "./wasm/web": { + "types": "./dist/wasm/web/truapi_server.d.ts", + "import": "./dist/wasm/web/truapi_server.js" + }, + "./wasm/node": { + "types": "./dist/wasm/node/truapi_server.d.ts", + "import": "./dist/wasm/node/truapi_server.js" + } + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc -b", + "build:wasm": "node scripts/build-wasm.mjs", + "prepare": "npm run build", + "test": "for f in test/*.test.mjs; do echo \"=== $f ===\" && node --test \"$f\" || exit 1; done" + }, + "dependencies": { + "@parity/truapi": "file:../truapi", + "@parity/truapi-host": "file:../truapi-host" + }, + "devDependencies": { + "typescript": "^5.7" + } +} diff --git a/js/packages/truapi-host-wasm/scripts/build-wasm.mjs b/js/packages/truapi-host-wasm/scripts/build-wasm.mjs new file mode 100644 index 00000000..ce20d027 --- /dev/null +++ b/js/packages/truapi-host-wasm/scripts/build-wasm.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +// Rebuild the truapi-server WASM artefacts generated under +// `dist/wasm/{web,node}/`. wasm-pack is required. + +import { execFile } from "node:child_process"; +import { rm } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pkgRoot = resolve(__dirname, ".."); +const repoRoot = resolve(pkgRoot, "../../.."); +const rustCrate = resolve(repoRoot, "rust/crates/truapi-server"); +const wasmProfile = process.env.TRUAPI_WASM_PROFILE ?? "release"; + +function args(target, outDir) { + const command = [ + "build", + "--target", + target, + "--out-dir", + outDir, + "--out-name", + "truapi_server", + ]; + if (wasmProfile === "dev") { + command.push("--dev"); + } else if (wasmProfile === "profiling") { + command.push("--profiling"); + } else if (wasmProfile !== "release") { + throw new Error( + `Unsupported TRUAPI_WASM_PROFILE=${wasmProfile}; expected release, dev, or profiling`, + ); + } + command.push(rustCrate, "--no-default-features"); + return command; +} + +async function build(target, subdir) { + const outDir = resolve(pkgRoot, "dist/wasm", subdir); + process.stdout.write( + `wasm-pack build --target ${target} --${wasmProfile} → ${outDir}\n`, + ); + try { + await execFileAsync("wasm-pack", args(target, outDir), { cwd: repoRoot }); + } catch (err) { + if (err?.code === "ENOENT") { + console.error( + "wasm-pack is required. Install it with `cargo install wasm-pack` " + + "or see https://rustwasm.github.io/wasm-pack/installer/", + ); + process.exit(1); + } + throw err; + } + // wasm-pack writes a nested `.gitignore: *`; the repo-level ignore already + // owns generated WASM outputs. + await rm(resolve(outDir, ".gitignore"), { force: true }); +} + +await build("web", "web"); +await build("nodejs", "node"); diff --git a/js/packages/truapi-host-wasm/src/dispatcher.ts b/js/packages/truapi-host-wasm/src/dispatcher.ts new file mode 100644 index 00000000..6de72e64 --- /dev/null +++ b/js/packages/truapi-host-wasm/src/dispatcher.ts @@ -0,0 +1,20 @@ +// Generic dispatcher utilities sit in `@parity/truapi-host`. This module +// re-exports them so hosts that depend on `@parity/truapi-host-wasm` get the +// dispatcher entry-point without a separate install. + +export { + createHostServer, + toFlatResponsePayload, + toResponsePayload, +} from "@parity/truapi-host"; + +export type { + CallContext, + HostDispatchEntry, + HostServerHooks, + RequestEntry, + SubscriptionCleanup, + SubscriptionEntry, + SubscriptionFramePort, + TrUApiHostServer, +} from "@parity/truapi-host"; diff --git a/js/packages/truapi-host-wasm/src/electron/index.ts b/js/packages/truapi-host-wasm/src/electron/index.ts new file mode 100644 index 00000000..9288419f --- /dev/null +++ b/js/packages/truapi-host-wasm/src/electron/index.ts @@ -0,0 +1,112 @@ +import type { Provider } from "@parity/truapi"; + +/** + * Minimal subset of Electron's `MessagePortMain` interface used by this + * package. Kept local so the package does not have a hard `electron` + * dependency (the host code passes the port in at runtime). + */ +export interface ElectronMessagePortMain { + postMessage(message: unknown, transfer?: unknown[]): void; + on(event: "message", handler: (event: { data: unknown }) => void): this; + on(event: "close", handler: () => void): this; + off(event: "message", handler: (event: { data: unknown }) => void): this; + off(event: "close", handler: () => void): this; + start(): void; + close(): void; +} + +/** + * Options for `createElectronProvider`. + */ +export interface CreateElectronProviderOptions { + /** One end of an Electron `MessageChannelMain`. The other end must be + * transferred to the renderer through the preload script. */ + port: ElectronMessagePortMain; +} + +/** + * Wrap an Electron `MessagePortMain` as a TrUAPI `Provider`. The + * provider exchanges SCALE-encoded `Uint8Array` frames with the renderer. + * The provider's `dispose` closes the port. + * + * Hosts typically pair this with `@parity/truapi-host-wasm`'s + * `createNodeWasmProvider` (for the WASM core) and `createHostServer` + * from `@parity/truapi-host` (for the dispatcher) to assemble a full + * Electron host. + */ +export function createElectronProvider( + options: CreateElectronProviderOptions, +): Provider { + const { port } = options; + const listeners = new Set<(message: Uint8Array) => void>(); + const closeListeners = new Set<(error: Error) => void>(); + let disposed = false; + let closedError: Error | null = null; + + const onMessage = (event: { data: unknown }): void => { + if (closedError) return; + const data = event.data; + if (!(data instanceof Uint8Array)) return; + for (const listener of [...listeners]) listener(data); + }; + + const removePortListeners = (): void => { + try { + port.off("message", onMessage); + port.off("close", onClose); + } catch { + // already detached + } + }; + + const close = (error: Error): void => { + if (closedError) return; + closedError = error; + removePortListeners(); + for (const listener of [...closeListeners]) listener(error); + listeners.clear(); + closeListeners.clear(); + }; + + const onClose = (): void => { + close(new Error("electron message port closed")); + }; + + port.on("message", onMessage); + port.on("close", onClose); + port.start(); + + return { + postMessage(bytes: Uint8Array): void { + if (closedError) return; + port.postMessage(bytes); + }, + subscribe(callback) { + if (closedError) return () => {}; + listeners.add(callback); + return () => { + listeners.delete(callback); + }; + }, + subscribeClose(callback) { + if (closedError) { + callback(closedError); + return () => {}; + } + closeListeners.add(callback); + return () => { + closeListeners.delete(callback); + }; + }, + dispose() { + if (disposed) return; + disposed = true; + try { + port.close(); + } catch { + // already closed + } + close(new Error("electron provider disposed")); + }, + }; +} diff --git a/js/packages/truapi-host-wasm/src/error-message.ts b/js/packages/truapi-host-wasm/src/error-message.ts new file mode 100644 index 00000000..b3583ffd --- /dev/null +++ b/js/packages/truapi-host-wasm/src/error-message.ts @@ -0,0 +1,5 @@ +export function errorMessage(err: unknown): string { + if (err instanceof Error) return err.message; + if (typeof err === "string") return err; + return JSON.stringify(err) ?? String(err); +} diff --git a/js/packages/truapi-host-wasm/src/index.ts b/js/packages/truapi-host-wasm/src/index.ts new file mode 100644 index 00000000..53821bb7 --- /dev/null +++ b/js/packages/truapi-host-wasm/src/index.ts @@ -0,0 +1,59 @@ +export type { + Payload, + ProtocolMessage, + Provider, + HostPermissionKind, +} from "./types.js"; + +export type { + AuthState, + Awaitable, + ChainConnect, + ChainConnection, + ChainProvider, + Features, + HostCallbacks, + LogLevel, + Navigation, + Notifications, + Permissions, + PreimageHost, + PlatformJsonRpcConnection, + SessionUiInfo, + HostStorage, + ThemeHost, + TrUApiHostWasmProvider, + WasmCoreLike, + WasmRawCallbacks, + WasmRuntimeConfig, +} from "./runtime.js"; +export { createUnavailableCallbacks, createWasmProvider } from "./runtime.js"; +export { createWasmRawCallbacks } from "./typed-callbacks.js"; + +export type { CreateNodeWasmProviderOptions } from "./node-runtime.js"; +export { createNodeWasmProvider } from "./node-runtime.js"; + +export type { + CallbackArgs, + CallbackName, + MainToWorker, + OptionalCallbackName, + SubscriptionName, + WorkerToMain, +} from "./worker-protocol.js"; + +export type { + CallContext, + HostDispatchEntry, + HostServerHooks, + RequestEntry, + SubscriptionCleanup, + SubscriptionEntry, + SubscriptionFramePort, + TrUApiHostServer, +} from "./dispatcher.js"; +export { + createHostServer, + toFlatResponsePayload, + toResponsePayload, +} from "./dispatcher.js"; diff --git a/js/packages/truapi-host-wasm/src/node-runtime.ts b/js/packages/truapi-host-wasm/src/node-runtime.ts new file mode 100644 index 00000000..cfcf53ac --- /dev/null +++ b/js/packages/truapi-host-wasm/src/node-runtime.ts @@ -0,0 +1,63 @@ +import { + createWasmProvider, + type LogLevel, + type TrUApiHostWasmProvider, + type WasmCoreLike, + type WasmRawCallbacks, + type WasmRuntimeConfig, +} from "./runtime.js"; + +interface NodeWasmModuleShape { + WasmTrUApiCore: new (callbacks: unknown, runtimeConfig: unknown) => WasmCoreLike; + setLogLevel: (level: string) => void; +} + +/** + * Options for `createNodeWasmProvider`. + */ +export interface CreateNodeWasmProviderOptions { + /** Wasm core log level. Default: `"off"`. */ + logLevel?: LogLevel; + /** Static product/pairing config passed to the Rust core. */ + runtimeConfig: WasmRuntimeConfig; +} + +/** + * Lazy-load the node-targeted WASM bundle and wrap it in a `Provider`. + * + * The bundle initialises synchronously (wasm-pack nodejs target uses + * `require()` under the hood for the .wasm file), so callers receive + * a ready-to-use provider once the dynamic import resolves. + */ +export async function createNodeWasmProvider( + partial: Omit, + options: CreateNodeWasmProviderOptions, +): Promise { + if (!options?.runtimeConfig) { + throw new Error("runtimeConfig is required"); + } + + // Dynamic import keeps the WASM module out of the package's static + // dependency graph and out of the tsc rootDir. Indirected through a + // variable so TS skips the static module-existence check. + const wasmNodePath = "./wasm/node/truapi_server.js"; + const mod = (await import( + /* @vite-ignore */ wasmNodePath + )) as NodeWasmModuleShape | { default: NodeWasmModuleShape }; + + const wasm: NodeWasmModuleShape = + "WasmTrUApiCore" in mod + ? (mod as NodeWasmModuleShape) + : (mod.default as NodeWasmModuleShape); + + if (!wasm?.WasmTrUApiCore) { + throw new Error("Node WASM bundle did not export WasmTrUApiCore"); + } + + wasm.setLogLevel?.(options.logLevel ?? "off"); + + return createWasmProvider( + (raw) => new wasm.WasmTrUApiCore(raw, options.runtimeConfig), + partial, + ); +} diff --git a/js/packages/truapi-host-wasm/src/runtime.ts b/js/packages/truapi-host-wasm/src/runtime.ts new file mode 100644 index 00000000..e2d5aab4 --- /dev/null +++ b/js/packages/truapi-host-wasm/src/runtime.ts @@ -0,0 +1,303 @@ +import type { Provider } from "@parity/truapi"; + +// The typed capability interfaces below come straight from the +// `truapi-platform` Rust crate via `truapi-codegen --platform-ts-output`. +// They are the host-author-facing surface: each method takes/returns +// typed wrappers (`HostDevicePermissionRequest`, etc.) rather than raw +// SCALE bytes. The `WasmRawCallbacks` interface declared further down +// is the byte-oriented wire surface the WASM core invokes; use +// `createWasmRawCallbacks` to adapt this typed surface into the raw +// callback surface consumed by `createWasmProvider`. +export type { + AuthState, + ChainProvider, + Features, + HostCallbacks, + JsonRpcConnection as PlatformJsonRpcConnection, + Navigation, + Notifications, + Permissions, + PreimageHost, + SessionUiInfo, + HostStorage, + ThemeHost, +} from "./generated/host-callbacks.js"; +import type { AuthState } from "./generated/host-callbacks.js"; + +/** + * Async-or-sync return. Synchronous hosts (e.g. the dotli main-thread + * shell hitting localStorage) can return a plain value; the WASM bridge + * awaits every return so an `async` impl also works. + */ +export type Awaitable = T | Promise; + +/** + * Open a JSON-RPC connection for `genesisHash`. The wasm bridge passes + * `onResponse` so the host can push smoldot replies back asynchronously. + * Returning `null` (or throwing) tells the core no provider is available. + */ +export type ChainConnect = ( + genesisHash: string, + onResponse: (json: string) => void, +) => Awaitable; + +/** + * Per-connection handle returned by `chainConnect`. `send` forwards a + * SCALE-encoded JSON-RPC request; `close` tears the connection down. + */ +export interface ChainConnection { + send(request: string): void; + close(): void; +} + +/** + * Verbosity threshold for the wasm core's `tracing` output. `off` silences + * it; the rest map to the matching browser `console` method (`debug`/`trace` + * land on `console.debug`, hidden in Chrome unless the console level dropdown + * includes "Verbose"). + */ +export type LogLevel = "off" | "error" | "warn" | "info" | "debug" | "trace"; + +export interface WasmRuntimeConfig { + productLabel: string; + productId: string; + siteId: string; + hostName: string; + hostIcon?: string; + hostVersion?: string; + platformType?: string; + platformVersion?: string; + peopleChainGenesisHash: string | Uint8Array; + pairingDeeplinkScheme: + | "polkadotapp" + | "polkadotApp" + | "PolkadotApp" + | "polkadotappdev" + | "polkadotAppDev" + | "PolkadotAppDev"; +} + +/** + * Raw byte-oriented callbacks the WASM core invokes. Names match the + * camelCase property keys the Rust `JsBridge::from_js` extracts. Request + * callbacks return `Promise` (or `Promise` for the + * permission prompts); subscription callbacks accept a `sendItem` sink + * and return an optional `dispose` function. + * + * This interface is the SCALE-byte-level wire surface between the WASM + * core and JS; the typed `HostCallbacks` interface above is the + * host-author surface. They overlap on the capability methods covered by + * `truapi-platform`; account, signing, and statement-store methods are owned + * by the Rust core and do not cross this callback boundary. + */ +export interface WasmRawCallbacks { + navigateTo(url: string): Promise; + pushNotification(payload: Uint8Array): Promise; + cancelNotification?(id: number): Promise; + devicePermission(payload: Uint8Array): Promise; + remotePermission(payload: Uint8Array): Promise; + featureSupported(payload: Uint8Array): Promise; + localStorageRead(key: string): Promise; + localStorageWrite(key: string, value: Uint8Array): Promise; + localStorageClear(key: string): Promise; + authStateChanged?(state: AuthState): void; + readSession?(): Promise; + writeSession?(value: Uint8Array): Promise; + clearSession?(): Promise; + subscribeSessionStore?(sendItem: () => void): (() => void) | void; + confirmSignPayload?(payload: Uint8Array): Promise; + confirmSignRaw?(payload: Uint8Array): Promise; + confirmCreateTransaction?(payload: Uint8Array): Promise; + confirmAccountAlias?(payload: Uint8Array): Promise; + confirmResourceAllocation?(payload: Uint8Array): Promise; + confirmPreimageSubmit?(size: number): Promise; + submitPreimage?(value: Uint8Array): Promise; + themeSubscribe?( + sendItem: (theme: "Light" | "Dark" | 0 | 1 | Uint8Array) => void, + ): (() => void) | void; + preimageLookupSubscribe( + key: Uint8Array, + sendItem: (value: Uint8Array | null | undefined) => void, + ): (() => void) | void; + /** Optional. When omitted, the WASM bridge reports chain calls as + * "unavailable". Hosts that own chain access (e.g. dotli's + * smoldot/RPC toggle) supply it. */ + chainConnect?: ChainConnect; + emitFrame(frame: Uint8Array): void; + dispose?(): void; +} + +/** + * Stubs every required callback so a host can spread them over its own + * implementation and override only what it supports. Unavailable methods + * reject with a descriptive error; unavailable subscriptions emit a current + * default item where the platform contract requires one. + */ +export function createUnavailableCallbacks(): Omit< + WasmRawCallbacks, + "emitFrame" | "dispose" | "chainConnect" +> { + const unavailable = (method: string) => async (): Promise => { + throw new Error(`${method} unavailable on this host`); + }; + const emitCurrentTick = (sendItem: () => void): void => { + sendItem(); + }; + const emitCurrentTheme = ( + sendItem: (theme: "Light" | "Dark" | 0 | 1 | Uint8Array) => void, + ): void => { + sendItem("Dark"); + }; + const emitCurrentPreimageMiss = ( + _key: Uint8Array, + sendItem: (value: Uint8Array | null | undefined) => void, + ): void => { + sendItem(undefined); + }; + return { + navigateTo: unavailable("navigateTo"), + pushNotification: async () => 0, + devicePermission: async () => false, + remotePermission: async () => false, + featureSupported: unavailable("featureSupported"), + localStorageRead: async () => undefined, + localStorageWrite: unavailable("localStorageWrite"), + localStorageClear: unavailable("localStorageClear"), + confirmPreimageSubmit: unavailable("confirmPreimageSubmit"), + submitPreimage: unavailable("submitPreimage"), + subscribeSessionStore: emitCurrentTick, + themeSubscribe: emitCurrentTheme, + preimageLookupSubscribe: emitCurrentPreimageMiss, + }; +} + +/** + * Shape exposed by the wasm-pack output's `WasmTrUApiCore`. Kept local + * so the package does not have a hard dependency on the generated `.d.ts` + * file path. + */ +export interface WasmCoreLike { + receiveFromProduct(frame: Uint8Array): Promise; + disconnect?(): Promise; + cancelLogin?(): void; + dispose(): void; + free(): void; +} + +export interface TrUApiHostWasmProvider extends Provider { + /** + * Core-owned logout/disconnect. This best-effort notifies the SSO peer, + * clears the in-memory session, clears SessionStore, and broadcasts + * Disconnected from the Rust core. + */ + disconnect(): Promise; + + /** + * Cancel any in-flight `requestLogin` pairing (e.g. the user closed the + * pairing UI). The core emits a `Disconnected` auth state and resolves + * the pending login as `Rejected`. A no-op when no login is in progress. + */ + cancelLogin(): void; + + /** + * Re-tune the wasm core's log level at runtime. Present on runtimes that + * keep a live channel to the core (e.g. the Web Worker provider); absent on + * one-shot constructions that only accept `logLevel` up front. + */ + setLogLevel?(level: LogLevel): void; +} + +/** + * Wraps a WASM core in a `Provider`, the byte transport abstraction + * exposed by `@parity/truapi`. The provider can be handed to + * `createHostServer` from `@parity/truapi-host` so the dispatcher dispatches + * inbound frames into the WASM core and forwards core-emitted frames back + * to the listener registered through `provider.subscribe`. + */ +export function createWasmProvider( + createCore: (rawCallbacks: WasmRawCallbacks) => WasmCoreLike, + partial: Omit, +): TrUApiHostWasmProvider { + const listeners = new Set<(message: Uint8Array) => void>(); + const closeListeners = new Set<(error: Error) => void>(); + let disposed = false; + let closedError: Error | null = null; + + // Terminal close-once transition, matching `createBaseProvider` in + // @parity/truapi: notify close listeners exactly once, then drop all + // listeners so the provider stops delivering. + const close = (error: Error): void => { + if (closedError) return; + closedError = error; + for (const listener of [...closeListeners]) listener(error); + listeners.clear(); + closeListeners.clear(); + }; + + const raw: WasmRawCallbacks = { + ...partial, + emitFrame(frame: Uint8Array) { + if (disposed || closedError) return; + // Copy out of the WASM-owned buffer so retained references stay + // valid once the core reuses the underlying memory. + const copy = new Uint8Array(frame.length); + copy.set(frame); + for (const listener of [...listeners]) listener(copy); + }, + }; + + const core = createCore(raw); + + return { + postMessage(bytes: Uint8Array): void { + if (disposed || closedError) return; + void core.receiveFromProduct(bytes).catch((err: unknown) => { + close(err instanceof Error ? err : new Error(String(err))); + }); + }, + subscribe(callback) { + if (closedError) return () => {}; + listeners.add(callback); + return () => { + listeners.delete(callback); + }; + }, + subscribeClose(callback) { + if (closedError) { + callback(closedError); + return () => {}; + } + closeListeners.add(callback); + return () => { + closeListeners.delete(callback); + }; + }, + async disconnect() { + if (disposed || closedError) return; + if (!core.disconnect) { + throw new Error("disconnect unavailable on this WASM core"); + } + await core.disconnect(); + }, + cancelLogin() { + if (disposed || closedError) return; + core.cancelLogin?.(); + }, + dispose() { + if (disposed) return; + disposed = true; + try { + core.dispose(); + } catch { + // host dispose threw, swallow during teardown + } + try { + core.free(); + } catch { + // already freed + } + close(new Error("wasm provider disposed")); + partial.dispose?.(); + }, + }; +} diff --git a/js/packages/truapi-host-wasm/src/subscription-table.ts b/js/packages/truapi-host-wasm/src/subscription-table.ts new file mode 100644 index 00000000..b76fd372 --- /dev/null +++ b/js/packages/truapi-host-wasm/src/subscription-table.ts @@ -0,0 +1,71 @@ +// Single source of truth for streaming host subscriptions crossing the +// worker boundary. Both the main-thread provider (callback dispatch and +// `optionalSubscriptions` advertisement) and the worker runtime (raw +// callback construction) derive their behavior from this table. + +import type { WasmRawCallbacks } from "./runtime.js"; +import type { SubscriptionName } from "./worker-protocol.js"; + +/** Host-side callback set the subscription adapters dispatch into. */ +export type SubscriptionCallbacks = Omit; + +/** Pushes one subscription item back to the worker. */ +export type PushItem = (value?: unknown) => void; + +type StartResult = (() => void) | void; + +/** + * One streaming subscription: the `WasmRawCallbacks` key implementing it, + * its wire-protocol name, and a `start` adapter typed per entry. Entries + * with `payload: "required"` are only started with a non-null payload. + */ +export type SubscriptionDispatchEntry = { + readonly callback: keyof SubscriptionCallbacks; + readonly protocol: SubscriptionName; +} & ( + | { + readonly payload: "none"; + readonly start: ( + callbacks: SubscriptionCallbacks, + push: PushItem, + ) => StartResult; + } + | { + readonly payload: "required"; + readonly start: ( + callbacks: SubscriptionCallbacks, + payload: Uint8Array, + push: PushItem, + ) => StartResult; + } +); + +/** Every streaming subscription the worker bridge knows how to dispatch. */ +export const SUBSCRIPTION_DISPATCH: readonly SubscriptionDispatchEntry[] = [ + { + callback: "subscribeSessionStore", + protocol: "sessionStoreSubscribe", + payload: "none", + start: (callbacks, push) => callbacks.subscribeSessionStore?.(push), + }, + { + callback: "themeSubscribe", + protocol: "themeSubscribe", + payload: "none", + start: (callbacks, push) => callbacks.themeSubscribe?.(push), + }, + { + callback: "preimageLookupSubscribe", + protocol: "preimageLookupSubscribe", + payload: "required", + start: (callbacks, payload, push) => + callbacks.preimageLookupSubscribe(payload, push), + }, +]; + +/** Looks up the dispatch entry for a wire subscription name, if known. */ +export function subscriptionDispatchEntry( + name: SubscriptionName, +): SubscriptionDispatchEntry | undefined { + return SUBSCRIPTION_DISPATCH.find((entry) => entry.protocol === name); +} diff --git a/js/packages/truapi-host-wasm/src/typed-callbacks.ts b/js/packages/truapi-host-wasm/src/typed-callbacks.ts new file mode 100644 index 00000000..82417db9 --- /dev/null +++ b/js/packages/truapi-host-wasm/src/typed-callbacks.ts @@ -0,0 +1,257 @@ +import { + HostDevicePermissionRequest, + HostFeatureSupportedRequest, + HostPushNotificationRequest, + RemotePermissionRequest, + ThemeVariant, + type GenericError, + type Result, +} from "@parity/truapi"; +import { hexToBytes } from "@parity/truapi/scale"; + +import { + createUnavailableCallbacks, + type ChainConnection, + type WasmRawCallbacks, +} from "./runtime.js"; +import type { AuthState, HostCallbacks } from "./generated/host-callbacks.js"; + +type WireResult = + | { success: true; value: T } + | { success: false; value: E }; + +type StreamResult = Result | WireResult; + +type MaybeAsyncIterable = AsyncIterable | Iterable; + +type OptionalTypedCallbacks = Partial; + +type RawWithoutEmit = Omit; + +const decodePushNotification = HostPushNotificationRequest.dec; +const decodeDevicePermission = HostDevicePermissionRequest.dec; +const decodeRemotePermission = RemotePermissionRequest.dec; +const decodeFeatureSupported = HostFeatureSupportedRequest.dec; + +function errorReason(error: GenericError): string { + return error.reason; +} + +function unwrapStreamResult(item: StreamResult): T { + if ("success" in item) { + if (item.success === false) { + throw new Error(errorReason(item.value)); + } + return item.value; + } + if (item.isErr()) { + throw new Error(errorReason(item.error)); + } + return item.value; +} + +function toAsyncIterator(stream: MaybeAsyncIterable): AsyncIterator { + const asyncIterable = stream as AsyncIterable; + if (typeof asyncIterable[Symbol.asyncIterator] === "function") { + return asyncIterable[Symbol.asyncIterator](); + } + + const iterator = (stream as Iterable)[Symbol.iterator](); + const asyncIterator: AsyncIterator = { + next: async () => iterator.next(), + }; + if (iterator.return) { + asyncIterator.return = async () => iterator.return!(); + } + return asyncIterator; +} + +function driveResultStream( + stream: MaybeAsyncIterable>, + sendItem: (value: T) => void, +): () => void { + const iterator = toAsyncIterator(stream); + let stopped = false; + void (async () => { + try { + while (!stopped) { + const next = await iterator.next(); + if (next.done) return; + sendItem(unwrapStreamResult(next.value)); + } + } catch (err) { + console.error("[truapi typed callbacks] subscription failed:", err); + } + })(); + return () => { + stopped = true; + void iterator.return?.(); + }; +} + +function chainConnect( + callbacks: OptionalTypedCallbacks, +): RawWithoutEmit["chainConnect"] { + if (!callbacks.connect) return undefined; + return async (genesisHash, onResponse): Promise => { + const connection = await callbacks.connect!(hexToBytes(genesisHash)); + const iterator = connection.responses()[Symbol.asyncIterator](); + let closed = false; + void (async () => { + try { + while (!closed) { + const next = await iterator.next(); + if (next.done) return; + onResponse(next.value); + } + } catch (err) { + console.error("[truapi typed callbacks] chain responses failed:", err); + } + })(); + return { + send(request: string): void { + connection.send(request); + }, + close(): void { + closed = true; + void iterator.return?.(); + }, + }; + }; +} + +/** + * Adapt generated typed host callbacks into the raw SCALE-byte callback + * surface consumed by the WASM core. + */ +export function createWasmRawCallbacks( + callbacks: OptionalTypedCallbacks, +): RawWithoutEmit { + const unavailable = createUnavailableCallbacks(); + const connect = chainConnect(callbacks); + return { + ...unavailable, + navigateTo: callbacks.navigateTo + ? (url) => callbacks.navigateTo!(url) + : unavailable.navigateTo, + pushNotification: callbacks.pushNotification + ? async (payload) => { + const response = await callbacks.pushNotification!( + decodePushNotification(payload), + ); + return response.id; + } + : unavailable.pushNotification, + ...(callbacks.cancelNotification + ? { + cancelNotification: (id: number) => callbacks.cancelNotification!(id), + } + : {}), + devicePermission: callbacks.devicePermission + ? async (payload) => { + const response = await callbacks.devicePermission!( + decodeDevicePermission(payload), + ); + return response.granted; + } + : unavailable.devicePermission, + remotePermission: callbacks.remotePermission + ? async (payload) => { + const response = await callbacks.remotePermission!( + decodeRemotePermission(payload), + ); + return response.granted; + } + : unavailable.remotePermission, + featureSupported: callbacks.featureSupported + ? async (payload) => { + const response = await callbacks.featureSupported!( + decodeFeatureSupported(payload), + ); + return response.supported; + } + : unavailable.featureSupported, + localStorageRead: callbacks.read + ? (key) => callbacks.read!(key) + : unavailable.localStorageRead, + localStorageWrite: callbacks.write + ? (key, value) => callbacks.write!(key, value) + : unavailable.localStorageWrite, + localStorageClear: callbacks.clear + ? (key) => callbacks.clear!(key) + : unavailable.localStorageClear, + ...(callbacks.authStateChanged + ? { + authStateChanged: (state: AuthState) => + callbacks.authStateChanged!(state), + } + : {}), + ...(callbacks.readSession + ? { readSession: () => callbacks.readSession!() } + : {}), + ...(callbacks.writeSession + ? { writeSession: (value: Uint8Array) => callbacks.writeSession!(value) } + : {}), + ...(callbacks.clearSession + ? { clearSession: () => callbacks.clearSession!() } + : {}), + ...(callbacks.subscribeSessionStore + ? { + subscribeSessionStore: (sendItem: () => void) => + driveResultStream(callbacks.subscribeSessionStore!(), () => + sendItem(), + ), + } + : {}), + ...(callbacks.confirmSignPayload + ? { + confirmSignPayload: (payload: Uint8Array) => + callbacks.confirmSignPayload!(payload), + } + : {}), + ...(callbacks.confirmSignRaw + ? { + confirmSignRaw: (payload: Uint8Array) => + callbacks.confirmSignRaw!(payload), + } + : {}), + ...(callbacks.confirmCreateTransaction + ? { + confirmCreateTransaction: (payload: Uint8Array) => + callbacks.confirmCreateTransaction!(payload), + } + : {}), + ...(callbacks.confirmAccountAlias + ? { + confirmAccountAlias: (payload: Uint8Array) => + callbacks.confirmAccountAlias!(payload), + } + : {}), + ...(callbacks.confirmResourceAllocation + ? { + confirmResourceAllocation: (payload: Uint8Array) => + callbacks.confirmResourceAllocation!(payload), + } + : {}), + confirmPreimageSubmit: callbacks.confirmPreimageSubmit + ? (size) => callbacks.confirmPreimageSubmit!(BigInt(size)) + : unavailable.confirmPreimageSubmit, + submitPreimage: callbacks.submitPreimage + ? (value) => callbacks.submitPreimage!(value) + : unavailable.submitPreimage, + ...(callbacks.subscribeTheme + ? { + themeSubscribe: (sendItem: (theme: ThemeVariant) => void) => + driveResultStream(callbacks.subscribeTheme!(), sendItem), + } + : {}), + preimageLookupSubscribe: callbacks.lookupPreimage + ? (key, sendItem) => + driveResultStream(callbacks.lookupPreimage!(key), (item) => { + const value = item as Uint8Array | undefined; + sendItem(value); + }) + : unavailable.preimageLookupSubscribe, + ...(connect ? { chainConnect: connect } : {}), + }; +} diff --git a/js/packages/truapi-host-wasm/src/types.ts b/js/packages/truapi-host-wasm/src/types.ts new file mode 100644 index 00000000..6ee1216c --- /dev/null +++ b/js/packages/truapi-host-wasm/src/types.ts @@ -0,0 +1,9 @@ +import type { Payload, ProtocolMessage, Provider } from "@parity/truapi"; + +export type { Payload, ProtocolMessage, Provider }; + +/** + * Subset of permission tags the host can be asked to prompt for. Mirrors + * the Rust `Permission` enum that flows through the WASM bridge. + */ +export type HostPermissionKind = "Device" | "Remote"; diff --git a/js/packages/truapi-host-wasm/src/web/create-iframe-host.ts b/js/packages/truapi-host-wasm/src/web/create-iframe-host.ts new file mode 100644 index 00000000..da9ef0ac --- /dev/null +++ b/js/packages/truapi-host-wasm/src/web/create-iframe-host.ts @@ -0,0 +1,146 @@ +import type { Provider } from "@parity/truapi"; + +/** + * Options for `createIframeHost`. + */ +export interface IframeHostOptions { + /** URL of the product iframe. */ + iframeUrl: string; + /** Container element the iframe is appended to. */ + container: HTMLElement; + /** + * Called with one end of the MessageChannel once the iframe has loaded. + * Hosts typically pipe this into a `Provider` (e.g. via + * `createMessagePortProvider` from `@parity/truapi`) and hand the + * provider to `createHostServer`. + */ + onPort: (port: MessagePort) => void; + /** + * Optional explicit allow-list origin. Defaults to the origin of + * `iframeUrl`. Throws if it disagrees with the iframe URL's origin. + */ + allowedOrigin?: string; + /** Override the default iframe sandbox attribute. */ + sandbox?: string; +} + +/** + * Handle returned by `createIframeHost`. + */ +export interface IframeHost { + iframe: HTMLIFrameElement; + dispose: () => void; +} + +const DEFAULT_IFRAME_SANDBOX = "allow-forms allow-same-origin allow-scripts"; +type CredentiallessIframe = HTMLIFrameElement & { credentialless?: boolean }; + +function resolveAllowedOrigin( + iframeUrl: string, + allowedOrigin?: string, +): string { + const targetUrl = new URL(iframeUrl, window.location.href); + if (targetUrl.protocol !== "http:" && targetUrl.protocol !== "https:") { + throw new Error( + `Iframe host only allows http(s) playground URLs, received ${targetUrl.protocol}`, + ); + } + + if (!allowedOrigin) { + return targetUrl.origin; + } + + const normalizedOrigin = new URL(allowedOrigin).origin; + if (normalizedOrigin !== targetUrl.origin) { + throw new Error( + `Iframe host origin policy mismatch, expected ${normalizedOrigin}, got ${targetUrl.origin}`, + ); + } + + return normalizedOrigin; +} + +/** + * Embed a product iframe and transfer a `MessagePort` into it. The host + * keeps the other end and passes it to a `Provider` (typically via + * `createMessagePortProvider`). All product traffic flows over the + * MessageChannel. + */ +export function createIframeHost(options: IframeHostOptions): IframeHost { + const { + iframeUrl, + container, + onPort, + allowedOrigin, + sandbox = DEFAULT_IFRAME_SANDBOX, + } = options; + + const channel = new MessageChannel(); + const hostPort = channel.port1; + const productPort = channel.port2; + const targetOrigin = resolveAllowedOrigin(iframeUrl, allowedOrigin); + + // Hand the host-side port to the caller immediately so it can wire up + // a provider before the iframe finishes loading. Queued postMessage + // calls are delivered once the channel is started by the provider. + onPort(hostPort); + + const iframe = document.createElement("iframe"); + iframe.style.width = "100%"; + iframe.style.height = "100%"; + iframe.style.border = "none"; + // COEP hosts need credentialless product iframes when the product origin + // does not serve matching embedder headers. + const credentiallessIframe = iframe as CredentiallessIframe; + credentiallessIframe.credentialless = true; + iframe.setAttribute("sandbox", sandbox); + iframe.referrerPolicy = "no-referrer"; + iframe.src = iframeUrl; + const initTargetOrigin = credentiallessIframe.credentialless + ? "*" + : targetOrigin; + + let initSent = false; + const sendInit = (): void => { + if (initSent) return; + const contentWindow = iframe.contentWindow; + if (!contentWindow) return; + initSent = true; + contentWindow.postMessage({ type: "truapi-init" }, initTargetOrigin, [ + productPort, + ]); + }; + + const onWindowMessage = (event: MessageEvent): void => { + if (event.source !== iframe.contentWindow) return; + if (event.origin !== targetOrigin && event.origin !== "null") return; + if (event.data?.type === "truapi-playground-ready") { + sendInit(); + } + }; + window.addEventListener("message", onWindowMessage); + + container.appendChild(iframe); + + return { + iframe, + dispose() { + window.removeEventListener("message", onWindowMessage); + try { + hostPort.close(); + } catch { + // already closed + } + try { + productPort.close(); + } catch { + // already closed + } + iframe.remove(); + }, + }; +} + +// Suppress unused-symbol warning when consumers do not import Provider +// directly; declaring the type relationship keeps the contract visible. +export type { Provider }; diff --git a/js/packages/truapi-host-wasm/src/web/create-worker-host-runtime.ts b/js/packages/truapi-host-wasm/src/web/create-worker-host-runtime.ts new file mode 100644 index 00000000..5c5b35de --- /dev/null +++ b/js/packages/truapi-host-wasm/src/web/create-worker-host-runtime.ts @@ -0,0 +1,695 @@ +import type { + ChainConnection, + LogLevel, + MainToWorker, + SubscriptionName, + TrUApiHostWasmProvider, + WasmRuntimeConfig, + WasmRawCallbacks, + WorkerToMain, +} from "../index.js"; +import { + OPTIONAL_CALLBACK_NAMES, + type CallbackName, + type OptionalCallbackName, +} from "../worker-protocol.js"; +import { errorMessage } from "../error-message.js"; +import { + SUBSCRIPTION_DISPATCH, + subscriptionDispatchEntry, +} from "../subscription-table.js"; +import { decodeWireMessage, describeWireId } from "@parity/truapi"; +import { bytesToHex } from "@parity/truapi/scale"; + +interface WorkerProviderState { + worker: Worker; + rawCallbacks: WasmRawCallbacks; + listeners: Set<(message: Uint8Array) => void>; + closeListeners: Set<(error: Error) => void>; + subscriptionDisposers: Map void>; + chainConnections: Map; + pendingDisconnects: Map< + number, + { resolve: () => void; reject: (error: Error) => void } + >; + closedError: Error | null; + logLevel: LogLevel; + disposed: boolean; +} + +function debugLoggingEnabled(state: WorkerProviderState): boolean { + return state.logLevel === "debug" || state.logLevel === "trace"; +} + +let nextDisconnectRequestId = 0; + +/** localStorage key the dev log level is persisted under, so it survives reloads. */ +const DEV_LOG_LEVEL_KEY = "truapi:logLevel"; +const LOG_LEVELS: readonly LogLevel[] = [ + "off", + "error", + "warn", + "info", + "debug", + "trace", +]; + +function isLogLevel(value: string | null): value is LogLevel { + return value !== null && (LOG_LEVELS as readonly string[]).includes(value); +} + +/** Read the persisted dev log level. Returns null when unset or unavailable. */ +function readPersistedLogLevel(): LogLevel | null { + try { + const stored = globalThis.localStorage?.getItem(DEV_LOG_LEVEL_KEY); + return isLogLevel(stored) ? stored : null; + } catch { + return null; + } +} + +/** Persist the dev log level so it re-applies on the next reload. */ +function persistLogLevel(level: LogLevel): void { + try { + globalThis.localStorage?.setItem(DEV_LOG_LEVEL_KEY, level); + } catch { + // Storage unavailable (sandboxed iframe / privacy mode); the level still + // applies for the current session. + } +} + +let devLogLevelOverride: LogLevel | null = readPersistedLogLevel(); +const devGlobalProviders = new Set(); + +interface TrUApiDevConsole { + setLogLevel(level: LogLevel): void; + getLogLevel(): LogLevel | null; + getProviderCount(): number; +} + +function optionalCallbacks( + callbacks: Omit, +): OptionalCallbackName[] { + return OPTIONAL_CALLBACK_NAMES.filter( + (name) => typeof callbacks[name] === "function", + ); +} + +function optionalSubscriptions( + callbacks: Omit, +): SubscriptionName[] { + return SUBSCRIPTION_DISPATCH.filter( + ({ callback }) => typeof callbacks[callback] === "function", + ).map(({ protocol }) => protocol); +} + +function bytesToHexPreview(bytes: Uint8Array, maxBytes = 96): string { + const visible = bytes.subarray(0, maxBytes); + const suffix = + bytes.length > maxBytes ? `…(+${bytes.length - maxBytes})` : ""; + return `${bytesToHex(visible)}${suffix}`; +} + +function describeWireFrame(bytes: Uint8Array) { + const decoded = decodeWireMessage(bytes); + if (decoded.isErr()) { + return { + frameBytes: bytes.byteLength, + decodeError: decoded.error.message, + frameHex: bytesToHexPreview(bytes), + }; + } + const wireId = decoded.value.payload.id; + const payload = decoded.value.payload.value; + return { + frame: describeWireId(wireId), + requestId: decoded.value.requestId, + wireId, + frameBytes: bytes.byteLength, + payloadBytes: payload.byteLength, + payloadHex: bytesToHexPreview(payload), + }; +} + +function handleCallbackRequest( + state: WorkerProviderState, + msg: { + requestId: number; + name: CallbackName; + args: readonly unknown[]; + }, +): void { + // Own-property guard: `msg.name` is worker-supplied, never walk the + // prototype chain with it. + const fn = Object.hasOwn(state.rawCallbacks, msg.name) + ? ( + state.rawCallbacks as unknown as Record< + string, + (...args: readonly unknown[]) => unknown + > + )[msg.name] + : undefined; + if (!fn) { + const reply: MainToWorker = { + kind: "callbackResponse", + requestId: msg.requestId, + ok: false, + error: `unknown callback: ${msg.name}`, + }; + state.worker.postMessage(reply); + return; + } + Promise.resolve() + .then(() => fn(...msg.args)) + .then( + (value) => { + const reply: MainToWorker = { + kind: "callbackResponse", + requestId: msg.requestId, + ok: true, + value, + }; + state.worker.postMessage(reply); + }, + (err) => { + const reply: MainToWorker = { + kind: "callbackResponse", + requestId: msg.requestId, + ok: false, + error: errorMessage(err), + }; + state.worker.postMessage(reply); + }, + ); +} + +function handleSubscriptionStart( + state: WorkerProviderState, + msg: { + subId: number; + name: SubscriptionName; + payload: Uint8Array | null; + }, +): void { + const entry = subscriptionDispatchEntry(msg.name); + if (!entry) { + console.warn(`[truapi worker] unknown subscription: ${msg.name}`); + return; + } + const sendItem = (value?: unknown): void => { + if (state.disposed) return; + const post: MainToWorker = { + kind: "subscriptionItem", + subId: msg.subId, + value, + }; + state.worker.postMessage(post); + }; + let dispose: (() => void) | void; + try { + if (entry.payload === "required") { + if (msg.payload === null) { + console.warn( + `[truapi worker] ${msg.name} requires payload, none received`, + ); + return; + } + dispose = entry.start(state.rawCallbacks, msg.payload, sendItem); + } else { + dispose = entry.start(state.rawCallbacks, sendItem); + } + } catch (err) { + console.error(`[truapi worker] ${msg.name} threw on start:`, err); + return; + } + if (typeof dispose === "function") { + state.subscriptionDisposers.set(msg.subId, dispose); + } +} + +function handleSubscriptionStop( + state: WorkerProviderState, + msg: { subId: number }, +): void { + const dispose = state.subscriptionDisposers.get(msg.subId); + if (!dispose) return; + state.subscriptionDisposers.delete(msg.subId); + try { + dispose(); + } catch (err) { + console.warn("[truapi worker] subscription dispose threw:", err); + } +} + +async function handleChainConnectStart( + state: WorkerProviderState, + msg: { connId: number; genesisHash: string }, +): Promise { + const chainConnect = state.rawCallbacks.chainConnect; + if (!chainConnect) { + const reply: MainToWorker = { + kind: "chainConnectAck", + connId: msg.connId, + ok: false, + error: "host did not supply chainConnect", + }; + state.worker.postMessage(reply); + return; + } + const onResponse = (json: string): void => { + if (state.disposed) return; + const post: MainToWorker = { + kind: "chainResponse", + connId: msg.connId, + json, + }; + state.worker.postMessage(post); + }; + try { + const conn = await chainConnect(msg.genesisHash, onResponse); + if (!conn) { + const reply: MainToWorker = { + kind: "chainConnectAck", + connId: msg.connId, + ok: false, + error: `chainConnect returned null for genesisHash ${msg.genesisHash}`, + }; + state.worker.postMessage(reply); + return; + } + state.chainConnections.set(msg.connId, conn); + const reply: MainToWorker = { + kind: "chainConnectAck", + connId: msg.connId, + ok: true, + }; + state.worker.postMessage(reply); + } catch (err) { + const reply: MainToWorker = { + kind: "chainConnectAck", + connId: msg.connId, + ok: false, + error: errorMessage(err), + }; + state.worker.postMessage(reply); + } +} + +function handleChainSend( + state: WorkerProviderState, + msg: { connId: number; request: string }, +): void { + const conn = state.chainConnections.get(msg.connId); + if (!conn) return; + try { + if (debugLoggingEnabled(state)) { + console.debug("[truapi worker] chainSend", msg.connId, msg.request); + } + conn.send(msg.request); + } catch (err) { + console.warn("[truapi worker] chain send threw:", err); + } +} + +function handleChainClose( + state: WorkerProviderState, + msg: { connId: number }, +): void { + const conn = state.chainConnections.get(msg.connId); + if (!conn) return; + state.chainConnections.delete(msg.connId); + try { + conn.close(); + } catch (err) { + console.warn("[truapi worker] chain close threw:", err); + } +} + +function handleDisconnectResponse( + state: WorkerProviderState, + msg: + | { requestId: number; ok: true } + | { requestId: number; ok: false; error: string }, +): void { + const pending = state.pendingDisconnects.get(msg.requestId); + if (!pending) return; + state.pendingDisconnects.delete(msg.requestId); + if (msg.ok) { + pending.resolve(); + } else { + pending.reject(new Error(msg.error)); + } +} + +function rejectPendingDisconnects( + state: WorkerProviderState, + error: Error, +): void { + for (const pending of state.pendingDisconnects.values()) { + pending.reject(error); + } + state.pendingDisconnects.clear(); +} + +/** + * Shared terminal teardown for both `dispose()` and worker faults: rejects + * pending disconnects, runs subscription disposers, closes chain connections, + * and terminates the worker. A fault additionally notifies close listeners. + */ +function teardown( + state: WorkerProviderState, + error: Error, + fault: boolean, +): void { + if (state.disposed) return; + state.disposed = true; + state.closedError = error; + rejectPendingDisconnects(state, error); + for (const fn of state.subscriptionDisposers.values()) { + try { + fn(); + } catch { + // ignore during teardown + } + } + state.subscriptionDisposers.clear(); + for (const conn of state.chainConnections.values()) { + try { + conn.close(); + } catch { + // ignore during teardown + } + } + state.chainConnections.clear(); + if (fault) { + state.worker.terminate(); + } else { + try { + const post: MainToWorker = { kind: "dispose" }; + state.worker.postMessage(post); + } catch { + // ignore if worker already gone + } + // Give the worker a tick to free the core before terminating. + setTimeout(() => state.worker.terminate(), 0); + } + for (const listener of [...state.closeListeners]) listener(error); + state.listeners.clear(); + state.closeListeners.clear(); +} + +export interface CreateWebWorkerProviderOptions { + /** Wasm core log level. Default: `"off"`. */ + logLevel?: LogLevel; + /** Static product/pairing config passed to the Rust core. */ + runtimeConfig: WasmRuntimeConfig; + /** + * Milliseconds to wait for the worker to report `ready` before rejecting + * and terminating it. Default: 30000. + */ + initTimeoutMs?: number; +} + +/** + * Spawn the truapi-server WASM in `worker` and bridge it into a + * `Provider`. The provider can be handed to `createHostServer` from + * `@parity/truapi-host`. + * + * The caller is responsible for instantiating the Worker, Vite users + * typically import the worker entry-point with `?worker`: + * + * ```ts + * import HostWorker from "@parity/truapi-host-wasm/worker-runtime?worker"; + * const worker = new HostWorker(); + * const provider = await createWebWorkerProvider(worker, callbacks, { + * runtimeConfig, + * }); + * ``` + * + * Resolves once the worker reports `ready` and rejects if the WASM + * fails to load. + */ +export function createWebWorkerProvider( + worker: Worker, + callbacks: Omit, + options: CreateWebWorkerProviderOptions, +): Promise { + if (!options?.runtimeConfig) { + return Promise.reject(new Error("runtimeConfig is required")); + } + + return new Promise((resolve, reject) => { + const state: WorkerProviderState = { + worker, + // `emitFrame` is satisfied by the worker side; main thread never + // calls it. Fill in a no-op so the typed callback set is complete. + rawCallbacks: { + ...(callbacks as WasmRawCallbacks), + emitFrame: () => {}, + }, + listeners: new Set(), + closeListeners: new Set(), + subscriptionDisposers: new Map(), + chainConnections: new Map(), + pendingDisconnects: new Map(), + closedError: null, + logLevel: devLogLevelOverride ?? options.logLevel ?? "off", + disposed: false, + }; + + const onMessage = (ev: MessageEvent): void => { + const msg = ev.data; + switch (msg.kind) { + case "loaded": + break; + case "ready": + break; + case "fatalError": + console.error("[truapi worker]", msg.error); + notifyFault(new Error(`worker fatal error: ${msg.error}`)); + break; + case "frameError": + console.error("[truapi worker]", msg.error); + notifyFault(new Error(`worker frame error: ${msg.error}`)); + break; + case "disposeError": + console.warn("[truapi worker] dispose:", msg.error); + break; + case "frame": + if (debugLoggingEnabled(state)) { + console.debug( + "[truapi worker] frame <-", + describeWireFrame(msg.bytes), + ); + } + for (const listener of [...state.listeners]) listener(msg.bytes); + break; + case "disconnectResponse": + handleDisconnectResponse(state, msg); + break; + case "callbackRequest": + if (debugLoggingEnabled(state)) { + console.debug("[truapi worker] callbackRequest", msg.name); + } + handleCallbackRequest(state, msg); + break; + case "subscriptionStart": + handleSubscriptionStart(state, msg); + break; + case "subscriptionStop": + handleSubscriptionStop(state, msg); + break; + case "chainConnectStart": + if (debugLoggingEnabled(state)) { + console.debug("[truapi worker] chainConnectStart", msg.connId); + } + void handleChainConnectStart(state, msg); + break; + case "chainSend": + handleChainSend(state, msg); + break; + case "chainClose": + handleChainClose(state, msg); + break; + default: { + const { kind } = msg as { kind?: unknown }; + console.warn( + `[truapi worker] unknown worker message kind: ${String(kind)}`, + ); + } + } + }; + + const notifyFault = (error: Error): void => { + teardown(state, error, true); + }; + + const onError = (e: ErrorEvent): void => { + cleanupInit(); + worker.terminate(); + reject(new Error(`worker init failed: ${e.message}`)); + }; + + const onInitMessageError = (): void => { + cleanupInit(); + worker.terminate(); + reject(new Error("worker message could not be deserialized during init")); + }; + + const onRuntimeError = (e: ErrorEvent): void => { + console.error("[truapi worker]", e.message); + notifyFault(new Error(`worker error: ${e.message}`)); + }; + + const onMessageError = (): void => { + notifyFault(new Error("worker message could not be deserialized")); + }; + + const onInitMessage = (ev: MessageEvent): void => { + const msg = ev.data; + if (msg.kind === "loaded") { + const init: MainToWorker = { + kind: "init", + logLevel: devLogLevelOverride ?? options.logLevel ?? "off", + runtimeConfig: options.runtimeConfig, + optionalCallbacks: optionalCallbacks(callbacks), + optionalSubscriptions: optionalSubscriptions(callbacks), + chainConnect: typeof callbacks.chainConnect === "function", + }; + worker.postMessage(init); + } else if (msg.kind === "ready") { + cleanupInit(); + worker.addEventListener("message", onMessage); + // Surface a post-init worker fault (uncaught throw, OOM, killed + // worker) to close listeners for the provider's lifetime. + worker.addEventListener("error", onRuntimeError); + worker.addEventListener("messageerror", onMessageError); + const provider = buildProvider(state); + exposeDevGlobal(provider); + resolve(provider); + } else if (msg.kind === "fatalError") { + cleanupInit(); + worker.terminate(); + reject(new Error(`worker init reported error: ${msg.error}`)); + } + }; + + const cleanupInit = (): void => { + clearTimeout(initTimeout); + worker.removeEventListener("error", onError); + worker.removeEventListener("messageerror", onInitMessageError); + worker.removeEventListener("message", onInitMessage); + }; + + const timeoutMs = options.initTimeoutMs ?? 30_000; + const initTimeout = setTimeout(() => { + cleanupInit(); + worker.terminate(); + reject(new Error(`worker init timed out after ${timeoutMs}ms`)); + }, timeoutMs); + + worker.addEventListener("error", onError); + worker.addEventListener("messageerror", onInitMessageError); + worker.addEventListener("message", onInitMessage); + }); +} + +function buildProvider(state: WorkerProviderState): TrUApiHostWasmProvider { + const provider: TrUApiHostWasmProvider = { + postMessage(bytes: Uint8Array): void { + if (state.disposed) return; + const post: MainToWorker = { kind: "frame", bytes }; + if (debugLoggingEnabled(state)) { + console.debug("[truapi worker] frame ->", describeWireFrame(bytes)); + } + state.worker.postMessage(post); + }, + subscribe(callback) { + state.listeners.add(callback); + return () => { + state.listeners.delete(callback); + }; + }, + subscribeClose(callback) { + if (state.closedError) { + callback(state.closedError); + return () => {}; + } + state.closeListeners.add(callback); + return () => { + state.closeListeners.delete(callback); + }; + }, + disconnect(): Promise { + if (state.disposed) return Promise.resolve(); + return new Promise((resolve, reject) => { + const requestId = ++nextDisconnectRequestId; + state.pendingDisconnects.set(requestId, { resolve, reject }); + try { + const post: MainToWorker = { kind: "disconnect", requestId }; + state.worker.postMessage(post); + } catch (err) { + state.pendingDisconnects.delete(requestId); + reject(err instanceof Error ? err : new Error(String(err))); + } + }); + }, + cancelLogin(): void { + if (state.disposed) return; + const post: MainToWorker = { kind: "cancelLogin" }; + state.worker.postMessage(post); + }, + setLogLevel(level: LogLevel): void { + if (state.disposed) return; + state.logLevel = level; + const post: MainToWorker = { kind: "setLogLevel", level }; + state.worker.postMessage(post); + }, + dispose() { + devGlobalProviders.delete(provider); + teardown(state, new Error("provider disposed"), false); + }, + }; + return provider; +} + +/** + * Publish `globalThis.__truapi.setLogLevel(level)` so a developer can re-tune + * the wasm core's verbosity live from the browser console without a reload. The + * level is persisted to `localStorage["truapi:logLevel"]` and re-applied on the + * next load, so it survives refreshes. Pair with the DevTools console "Verbose" + * level to surface debug/trace. + */ +function exposeDevGlobal(provider: TrUApiHostWasmProvider): void { + devGlobalProviders.add(provider); + if (devLogLevelOverride !== null) { + provider.setLogLevel?.(devLogLevelOverride); + } + publishDevGlobal(); +} + +function publishDevGlobal(): void { + const target = globalThis as { + __truapi?: TrUApiDevConsole; + }; + target.__truapi = { + setLogLevel(level: LogLevel): void { + devLogLevelOverride = level; + persistLogLevel(level); + for (const provider of [...devGlobalProviders]) { + provider.setLogLevel?.(level); + } + console.info( + `[truapi worker] logLevel=${level} providers=${String(devGlobalProviders.size)}`, + ); + }, + getLogLevel(): LogLevel | null { + return devLogLevelOverride; + }, + getProviderCount(): number { + return devGlobalProviders.size; + }, + }; +} + +publishDevGlobal(); diff --git a/js/packages/truapi-host-wasm/src/web/index.ts b/js/packages/truapi-host-wasm/src/web/index.ts new file mode 100644 index 00000000..6876c6cc --- /dev/null +++ b/js/packages/truapi-host-wasm/src/web/index.ts @@ -0,0 +1,4 @@ +export type { IframeHost, IframeHostOptions } from "./create-iframe-host.js"; +export { createIframeHost } from "./create-iframe-host.js"; +export type { CreateWebWorkerProviderOptions } from "./create-worker-host-runtime.js"; +export { createWebWorkerProvider } from "./create-worker-host-runtime.js"; diff --git a/js/packages/truapi-host-wasm/src/worker-protocol.ts b/js/packages/truapi-host-wasm/src/worker-protocol.ts new file mode 100644 index 00000000..5f42a867 --- /dev/null +++ b/js/packages/truapi-host-wasm/src/worker-protocol.ts @@ -0,0 +1,121 @@ +// Wire format between the main thread (`createWebWorkerProvider`) and the +// Web Worker that hosts the truapi-server WASM core. +// +// Frames (`kind: 'frame'`) carry SCALE-encoded `ProtocolMessage` bytes +// untouched in either direction. Everything else is a control message +// for callback dispatch, subscription bookkeeping, or chain connections. +// +// Frame bytes cross the boundary by structured clone, deliberately not as +// transferables: the sender keeps using its buffer (the worker side posts +// views into WASM memory) and frames are small, so the copy is the simpler +// safe choice. + +import type { LogLevel } from "./runtime.js"; + +/** + * Names of every request/response style host callback the wasm core can + * invoke. Names match the camelCase property keys of `WasmRawCallbacks`. + */ +export const CALLBACK_NAMES = [ + "navigateTo", + "pushNotification", + "cancelNotification", + "devicePermission", + "remotePermission", + "featureSupported", + "localStorageRead", + "localStorageWrite", + "localStorageClear", + "authStateChanged", + "readSession", + "writeSession", + "clearSession", + "confirmSignPayload", + "confirmSignRaw", + "confirmCreateTransaction", + "confirmAccountAlias", + "confirmResourceAllocation", + "confirmPreimageSubmit", + "submitPreimage", +] as const; + +export type CallbackName = (typeof CALLBACK_NAMES)[number]; + +export const OPTIONAL_CALLBACK_NAMES = [ + "cancelNotification", + "authStateChanged", + "readSession", + "writeSession", + "clearSession", + "confirmSignPayload", + "confirmSignRaw", + "confirmCreateTransaction", + "confirmAccountAlias", + "confirmResourceAllocation", + "confirmPreimageSubmit", + "submitPreimage", +] as const satisfies readonly CallbackName[]; + +export type OptionalCallbackName = (typeof OPTIONAL_CALLBACK_NAMES)[number]; + +/** + * Names of every subscription host callback. Each has the shape + * `(payload?, sendItem) => dispose | void`. + */ +export type SubscriptionName = + | "sessionStoreSubscribe" + | "preimageLookupSubscribe" + | "themeSubscribe"; + +/** + * Positional arguments for a callback. The wasm core calls each callback + * at a fixed arity; a uniform `unknown[]` keeps the wire protocol simple. + */ +export type CallbackArgs = readonly unknown[]; + +export type MainToWorker = + | { + kind: "init"; + logLevel: LogLevel; + runtimeConfig: unknown; + optionalCallbacks?: readonly OptionalCallbackName[]; + optionalSubscriptions?: readonly SubscriptionName[]; + chainConnect?: boolean; + } + | { kind: "setLogLevel"; level: LogLevel } + | { kind: "frame"; bytes: Uint8Array } + | { kind: "disconnect"; requestId: number } + | { kind: "cancelLogin" } + | { kind: "callbackResponse"; requestId: number; ok: true; value: unknown } + | { kind: "callbackResponse"; requestId: number; ok: false; error: string } + | { kind: "subscriptionItem"; subId: number; value: unknown } + | { kind: "chainConnectAck"; connId: number; ok: true } + | { kind: "chainConnectAck"; connId: number; ok: false; error: string } + | { kind: "chainResponse"; connId: number; json: string } + | { kind: "dispose" }; + +export type WorkerToMain = + | { kind: "loaded" } + | { kind: "ready" } + | { kind: "fatalError"; error: string } + | { kind: "frameError"; error: string } + | { kind: "disposeError"; error: string } + | { kind: "frame"; bytes: Uint8Array } + | { kind: "disconnectResponse"; requestId: number; ok: true } + | { kind: "disconnectResponse"; requestId: number; ok: false; error: string } + | { + kind: "callbackRequest"; + requestId: number; + name: CallbackName; + args: CallbackArgs; + } + | { + kind: "subscriptionStart"; + subId: number; + name: SubscriptionName; + payload: Uint8Array | null; + } + | { kind: "subscriptionStop"; subId: number } + | { kind: "chainConnectStart"; connId: number; genesisHash: string } + | { kind: "chainSend"; connId: number; request: string } + | { kind: "chainClose"; connId: number }; diff --git a/js/packages/truapi-host-wasm/src/worker-runtime.ts b/js/packages/truapi-host-wasm/src/worker-runtime.ts new file mode 100644 index 00000000..f1413d46 --- /dev/null +++ b/js/packages/truapi-host-wasm/src/worker-runtime.ts @@ -0,0 +1,341 @@ +/// +// Worker entrypoint. Loads the web-targeted truapi-server WASM bundle and +// bridges every host callback over postMessage. The main thread keeps the +// state that needs DOM access (localStorage, prompts) while the CPU-heavy +// smoldot/dispatcher work runs here off the page main thread. + +import type { + CallbackName, + MainToWorker, + OptionalCallbackName, + SubscriptionName, + WorkerToMain, +} from "./worker-protocol.js"; +import { errorMessage } from "./error-message.js"; +import { SUBSCRIPTION_DISPATCH } from "./subscription-table.js"; + +interface WasmCore { + receiveFromProduct(frame: Uint8Array): Promise; + disconnect(): Promise; + cancelLogin(): void; + dispose(): void; + free(): void; +} + +interface WasmModuleShape { + default: (input?: unknown) => Promise; + WasmTrUApiCore: new (callbacks: unknown, runtimeConfig: unknown) => WasmCore; + setLogLevel?: (level: string) => void; +} + +// Resolved at runtime, the wasm-pack artifact lives outside `src/` so a +// static import would leak into the TS rootDir. The relative path is +// resolved against `dist/worker-runtime.js` once compiled. Indirected +// through a variable so TS skips the static module-existence check. +const WASM_WEB_PATH = "./wasm/web/truapi_server.js"; +const wasmModulePromise = import( + /* @vite-ignore */ WASM_WEB_PATH +) as Promise; + +const ctx = self as unknown as DedicatedWorkerGlobalScope; + +function postToMain(msg: WorkerToMain): void { + ctx.postMessage(msg); +} + +let nextRequestId = 0; +const pendingCallbacks = new Map< + number, + ( + result: { ok: true; value: unknown } | { ok: false; error: string }, + ) => void +>(); + +let nextSubId = 0; +const subscriptionItemListeners = new Map< + number, + (value: unknown) => void +>(); + +let nextConnId = 0; +type ChainConnectAck = { ok: true } | { ok: false; error: string }; +const chainConnectAcks = new Map void>(); +const chainResponseListeners = new Map void>(); + +function callbackRequest( + name: CallbackName, + args: readonly unknown[], +): Promise { + return new Promise((resolve, reject) => { + const requestId = ++nextRequestId; + pendingCallbacks.set(requestId, (r) => { + if (r.ok) resolve(r.value); + else reject(new Error(r.error)); + }); + postToMain({ kind: "callbackRequest", requestId, name, args }); + }); +} + +function startSubscription( + name: SubscriptionName, + payload: Uint8Array | null, + sendItem: (value: T) => void, +): () => void { + const subId = ++nextSubId; + subscriptionItemListeners.set(subId, sendItem as (value: unknown) => void); + postToMain({ kind: "subscriptionStart", subId, name, payload }); + return () => { + subscriptionItemListeners.delete(subId); + postToMain({ kind: "subscriptionStop", subId }); + }; +} + +interface WorkerChainConnection { + send(request: string): void; + close(): void; +} + +function chainConnect( + genesisHash: string, + onResponse: (json: string) => void, +): Promise { + const connId = ++nextConnId; + return new Promise((resolve, reject) => { + chainConnectAcks.set(connId, (ack) => { + if (!ack.ok) { + chainResponseListeners.delete(connId); + reject(new Error(ack.error)); + return; + } + resolve({ + send(request: string) { + postToMain({ kind: "chainSend", connId, request }); + }, + close() { + chainResponseListeners.delete(connId); + postToMain({ kind: "chainClose", connId }); + }, + }); + }); + chainResponseListeners.set(connId, onResponse); + postToMain({ kind: "chainConnectStart", connId, genesisHash }); + }); +} + +type RawCallbackFn = (...args: never[]) => unknown; + +const requiredRawCallbacks: Record = { + navigateTo: (url: string) => callbackRequest("navigateTo", [url]), + pushNotification: (payload: Uint8Array) => + callbackRequest("pushNotification", [payload]), + devicePermission: (payload: Uint8Array) => + callbackRequest("devicePermission", [payload]) as Promise, + remotePermission: (payload: Uint8Array) => + callbackRequest("remotePermission", [payload]) as Promise, + featureSupported: (payload: Uint8Array) => + callbackRequest("featureSupported", [payload]) as Promise, + localStorageRead: (key: string) => + callbackRequest("localStorageRead", [key]) as Promise< + Uint8Array | null | undefined + >, + localStorageWrite: (key: string, value: Uint8Array) => + callbackRequest("localStorageWrite", [key, value]), + localStorageClear: (key: string) => + callbackRequest("localStorageClear", [key]), +}; + +const optionalRawCallbacks: Record = { + cancelNotification: (id: number) => + callbackRequest("cancelNotification", [id]), + // Fire-and-forget notification: the wasm core ignores the returned promise. + authStateChanged: (state: unknown) => + void callbackRequest("authStateChanged", [state]).catch(() => {}), + readSession: () => + callbackRequest("readSession", []) as Promise< + Uint8Array | null | undefined + >, + writeSession: (value: Uint8Array) => + callbackRequest("writeSession", [value]), + clearSession: () => callbackRequest("clearSession", []), + confirmSignPayload: (payload: Uint8Array) => + callbackRequest("confirmSignPayload", [payload]) as Promise, + confirmSignRaw: (payload: Uint8Array) => + callbackRequest("confirmSignRaw", [payload]) as Promise, + confirmCreateTransaction: (payload: Uint8Array) => + callbackRequest("confirmCreateTransaction", [payload]) as Promise, + confirmAccountAlias: (payload: Uint8Array) => + callbackRequest("confirmAccountAlias", [payload]) as Promise, + confirmResourceAllocation: (payload: Uint8Array) => + callbackRequest("confirmResourceAllocation", [payload]) as Promise, + confirmPreimageSubmit: (size: number) => + callbackRequest("confirmPreimageSubmit", [size]) as Promise, + submitPreimage: (value: Uint8Array) => + callbackRequest("submitPreimage", [value]) as Promise, +}; + +function buildRawCallbacks(msg: Extract) { + const callbacks: Record = { ...requiredRawCallbacks }; + for (const name of msg.optionalCallbacks ?? []) { + callbacks[name] = optionalRawCallbacks[name]; + } + const optionalSubscriptions = new Set(msg.optionalSubscriptions ?? []); + for (const entry of SUBSCRIPTION_DISPATCH) { + if (!optionalSubscriptions.has(entry.protocol)) continue; + callbacks[entry.callback] = + entry.payload === "required" + ? (payload: Uint8Array, sendItem: (value: unknown) => void) => + startSubscription(entry.protocol, payload, sendItem) + : (sendItem: (value: unknown) => void) => + startSubscription(entry.protocol, null, sendItem); + } + if (msg.chainConnect) { + callbacks.chainConnect = chainConnect; + } + callbacks.emitFrame = (frame: Uint8Array): void => { + postToMain({ kind: "frame", bytes: frame }); + }; + callbacks.dispose = (): void => { + // Main thread terminates the worker, no separate cleanup needed here. + }; + return callbacks; +} + +let core: WasmCore | null = null; +let wasm: WasmModuleShape | null = null; + +(async () => { + try { + wasm = await wasmModulePromise; + await wasm.default(); + postToMain({ kind: "loaded" }); + } catch (err) { + postToMain({ kind: "fatalError", error: errorMessage(err) }); + } +})(); + +ctx.addEventListener("message", (ev: MessageEvent) => { + const msg = ev.data; + switch (msg.kind) { + case "init": + if (!wasm) { + postToMain({ + kind: "fatalError", + error: "init received before WASM loaded", + }); + break; + } + if (core) { + postToMain({ + kind: "fatalError", + error: "init: core already initialized", + }); + break; + } + wasm.setLogLevel?.(msg.logLevel); + try { + core = new wasm.WasmTrUApiCore(buildRawCallbacks(msg), msg.runtimeConfig); + postToMain({ kind: "ready" }); + } catch (err) { + postToMain({ kind: "fatalError", error: `init: ${errorMessage(err)}` }); + } + break; + case "setLogLevel": + wasm?.setLogLevel?.(msg.level); + break; + case "frame": + void handleFrame(msg.bytes); + break; + case "disconnect": + void handleDisconnect(msg.requestId); + break; + case "cancelLogin": + core?.cancelLogin(); + break; + case "callbackResponse": { + const cb = pendingCallbacks.get(msg.requestId); + if (cb) { + pendingCallbacks.delete(msg.requestId); + cb( + msg.ok + ? { ok: true, value: msg.value } + : { ok: false, error: msg.error }, + ); + } + break; + } + case "subscriptionItem": { + const listener = subscriptionItemListeners.get(msg.subId); + if (listener) listener(msg.value); + break; + } + case "chainConnectAck": { + const cb = chainConnectAcks.get(msg.connId); + if (cb) { + chainConnectAcks.delete(msg.connId); + cb(msg.ok ? { ok: true } : { ok: false, error: msg.error }); + } + break; + } + case "chainResponse": { + const listener = chainResponseListeners.get(msg.connId); + if (listener) listener(msg.json); + break; + } + case "dispose": + try { + core?.dispose(); + core?.free(); + } catch (err) { + postToMain({ kind: "disposeError", error: errorMessage(err) }); + } + core = null; + break; + default: { + const { kind } = msg as { kind?: unknown }; + console.warn( + `[truapi worker-runtime] unknown message kind: ${String(kind)}`, + ); + } + } +}); + +async function handleDisconnect(requestId: number): Promise { + if (!core) { + postToMain({ + kind: "disconnectResponse", + requestId, + ok: false, + error: "disconnect received before core is ready", + }); + return; + } + try { + await core.disconnect(); + postToMain({ kind: "disconnectResponse", requestId, ok: true }); + } catch (err) { + postToMain({ + kind: "disconnectResponse", + requestId, + ok: false, + error: errorMessage(err), + }); + } +} + +async function handleFrame(bytes: Uint8Array): Promise { + if (!core) { + postToMain({ + kind: "frameError", + error: "frame received before core is ready", + }); + return; + } + try { + await core.receiveFromProduct(bytes); + } catch (err) { + postToMain({ + kind: "frameError", + error: errorMessage(err), + }); + } +} diff --git a/js/packages/truapi-host-wasm/test/create-iframe-host.test.mjs b/js/packages/truapi-host-wasm/test/create-iframe-host.test.mjs new file mode 100644 index 00000000..4659f4d4 --- /dev/null +++ b/js/packages/truapi-host-wasm/test/create-iframe-host.test.mjs @@ -0,0 +1,261 @@ +// Verify that `createIframeHost` hands a MessagePort back through `onPort`, +// constructs an iframe with the expected attributes, and posts the +// `truapi-init` handshake after the iframe reports readiness. + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { createIframeHost } from "../dist/web/index.js"; + +function setupFakeDom() { + // Track listeners on the synthetic `window` and the iframe so the + // test can simulate the iframe `load` event after construction. + const iframeListeners = new Map(); + const windowListeners = new Map(); + const windowRemove = test.mock.fn(); + const contentPostMessage = test.mock.fn(); + + const contentWindow = { + postMessage: contentPostMessage, + }; + + const iframe = { + style: {}, + setAttribute: test.mock.fn(), + addEventListener: (name, fn) => { + iframeListeners.set(name, fn); + }, + removeEventListener: () => {}, + remove: test.mock.fn(), + referrerPolicy: "", + credentialless: false, + src: "", + contentWindow, + }; + + const container = { + appendChild: test.mock.fn(), + }; + + // Spy on both MessageChannel ports so dispose() teardown is observable. + const port1 = { postMessage: test.mock.fn(), close: test.mock.fn() }; + const port2 = { postMessage: test.mock.fn(), close: test.mock.fn() }; + globalThis.MessageChannel = class { + constructor() { + this.port1 = port1; + this.port2 = port2; + } + }; + + globalThis.document = { + createElement: (tag) => { + assert.equal(tag, "iframe"); + return iframe; + }, + }; + globalThis.window = { + location: { href: "http://localhost:5174/" }, + addEventListener: (name, fn) => { + windowListeners.set(name, fn); + }, + removeEventListener: windowRemove, + }; + + return { + iframe, + container, + contentPostMessage, + contentWindow, + iframeListeners, + windowListeners, + windowRemove, + port1, + port2, + }; +} + +function teardownFakeDom() { + delete globalThis.document; + delete globalThis.window; + delete globalThis.MessageChannel; +} + +test("createIframeHost hands back a MessagePort and configures the iframe", () => { + const { + iframe, + container, + iframeListeners, + windowRemove, + port1, + port2, + } = setupFakeDom(); + + try { + let receivedPort = null; + const host = createIframeHost({ + iframeUrl: "http://localhost:5174/", + container, + onPort: (port) => { + receivedPort = port; + }, + }); + + assert.ok(receivedPort, "onPort must fire synchronously"); + assert.equal(typeof receivedPort.postMessage, "function"); + assert.equal(container.appendChild.mock.callCount(), 1); + assert.equal(host.iframe, iframe); + assert.equal(iframe.credentialless, true); + assert.equal(iframe.src, "http://localhost:5174/"); + assert.equal( + iframeListeners.has("load"), + false, + "port transfer waits for explicit iframe readiness", + ); + + host.dispose(); + assert.equal(iframe.remove.mock.callCount(), 1); + assert.equal( + windowRemove.mock.callCount(), + 1, + "dispose removes the window message listener", + ); + assert.equal(windowRemove.mock.calls[0].arguments[0], "message"); + assert.equal( + port1.close.mock.callCount(), + 1, + "host port closed on dispose", + ); + assert.equal( + port2.close.mock.callCount(), + 1, + "product port closed on dispose", + ); + } finally { + teardownFakeDom(); + } +}); + +test("createIframeHost sends truapi-init on a same-origin playground-ready message", () => { + const { contentPostMessage, windowListeners, contentWindow } = setupFakeDom(); + + try { + createIframeHost({ + iframeUrl: "http://localhost:5174/", + container: { appendChild: () => {} }, + onPort: () => {}, + }); + + const onMessage = windowListeners.get("message"); + assert.ok(onMessage, "window message listener must be registered"); + + // Wrong source is dropped. + onMessage({ + source: { other: true }, + origin: "http://localhost:5174", + data: { type: "truapi-playground-ready" }, + }); + assert.equal( + contentPostMessage.mock.callCount(), + 0, + "wrong source dropped", + ); + + // Wrong origin is dropped. + onMessage({ + source: contentWindow, + origin: "http://evil.example", + data: { type: "truapi-playground-ready" }, + }); + assert.equal( + contentPostMessage.mock.callCount(), + 0, + "wrong origin dropped", + ); + + // Correct source + origin triggers the init handshake. + onMessage({ + source: contentWindow, + origin: "http://localhost:5174", + data: { type: "truapi-playground-ready" }, + }); + assert.equal(contentPostMessage.mock.callCount(), 1, "ready triggers init"); + const [body, origin] = contentPostMessage.mock.calls[0].arguments; + assert.deepEqual(body, { type: "truapi-init" }); + assert.equal(origin, "*"); + + // The handshake is idempotent across repeated ready events too. + onMessage({ + source: contentWindow, + origin: "http://localhost:5174", + data: { type: "truapi-playground-ready" }, + }); + assert.equal(contentPostMessage.mock.callCount(), 1, "init sent only once"); + } finally { + teardownFakeDom(); + } +}); + +test("createIframeHost accepts playground-ready from a credentialless opaque origin", () => { + const { contentPostMessage, windowListeners, contentWindow } = setupFakeDom(); + + try { + createIframeHost({ + iframeUrl: "http://localhost:5174/", + container: { appendChild: () => {} }, + onPort: () => {}, + }); + + const onMessage = windowListeners.get("message"); + assert.ok(onMessage, "window message listener must be registered"); + + onMessage({ + source: contentWindow, + origin: "null", + data: { type: "truapi-playground-ready" }, + }); + assert.equal( + contentPostMessage.mock.callCount(), + 1, + "opaque credentialless origin triggers init", + ); + const [, origin] = contentPostMessage.mock.calls[0].arguments; + assert.equal(origin, "*"); + } finally { + teardownFakeDom(); + } +}); + +test("createIframeHost rejects a mismatched allowedOrigin", () => { + setupFakeDom(); + try { + assert.throws( + () => + createIframeHost({ + iframeUrl: "http://localhost:5174/", + container: { appendChild: () => {} }, + onPort: () => {}, + allowedOrigin: "http://localhost:9999", + }), + /origin policy mismatch/, + ); + } finally { + teardownFakeDom(); + } +}); + +test("createIframeHost rejects non-http(s) iframe URLs", () => { + setupFakeDom(); + try { + assert.throws( + () => + createIframeHost({ + iframeUrl: "file:///etc/passwd", + container: { appendChild: () => {} }, + onPort: () => {}, + }), + /only allows http\(s\)/, + ); + } finally { + teardownFakeDom(); + } +}); diff --git a/js/packages/truapi-host-wasm/test/dispatcher-roundtrip.test.mjs b/js/packages/truapi-host-wasm/test/dispatcher-roundtrip.test.mjs new file mode 100644 index 00000000..3b6d7bbc --- /dev/null +++ b/js/packages/truapi-host-wasm/test/dispatcher-roundtrip.test.mjs @@ -0,0 +1,205 @@ +// Smoke test that the dispatcher re-export from @parity/truapi-host-wasm +// routes inbound request frames to a registered handler and emits a +// response frame back through the provider, end-to-end. + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { encodeWireMessage, decodeWireMessage } from "@parity/truapi"; +import { createHostServer } from "../dist/index.js"; + +function makeRecordingProvider() { + const listeners = new Set(); + const closeListeners = new Set(); + const sent = []; + return { + sent, + provider: { + postMessage(bytes) { + sent.push(bytes); + }, + subscribe(callback) { + listeners.add(callback); + return () => listeners.delete(callback); + }, + subscribeClose(callback) { + closeListeners.add(callback); + return () => closeListeners.delete(callback); + }, + dispose() { + listeners.clear(); + closeListeners.clear(); + }, + }, + deliver(message) { + for (const listener of [...listeners]) listener(message); + }, + triggerClose(error) { + for (const listener of [...closeListeners]) listener(error); + }, + }; +} + +test("createHostServer dispatches a request id to the matching entry and emits a response", async () => { + const requestId = 7; + const responseId = 8; + const { provider, sent, deliver } = makeRecordingProvider(); + + const entries = [ + { + kind: "request", + ids: { request: requestId, response: responseId }, + async handle(ctx, payload) { + assert.equal(typeof ctx.requestId, "string"); + // Echo with one extra byte so the test asserts that the right + // bytes flowed through. + const out = new Uint8Array(payload.length + 1); + out.set(payload); + out[payload.length] = 42; + return out; + }, + }, + ]; + + const server = createHostServer(provider, entries); + + const inboundFrame = encodeWireMessage({ + requestId: "req-1", + payload: { id: requestId, value: new Uint8Array([1, 2, 3]) }, + }); + assert.ok(inboundFrame.isOk(), "inbound frame must encode"); + deliver(inboundFrame.value); + + // Allow the handler microtask + send to resolve. + await new Promise((r) => setImmediate(r)); + + assert.equal(sent.length, 1, "exactly one response emitted"); + const decoded = decodeWireMessage(sent[0]); + assert.ok(decoded.isOk(), "response frame must decode"); + assert.equal(decoded.value.requestId, "req-1"); + assert.equal(decoded.value.payload.id, responseId); + assert.deepEqual( + Array.from(decoded.value.payload.value), + [1, 2, 3, 42], + "payload should echo + extra byte", + ); + + server.dispose(); +}); + +test("a rejecting request handler triggers onRequestHandlerError and emits no frame", async () => { + const { provider, sent, deliver } = makeRecordingProvider(); + + const errors = []; + const entries = [ + { + kind: "request", + ids: { request: 7, response: 8 }, + async handle() { + throw new Error("handler boom"); + }, + }, + ]; + const server = createHostServer(provider, entries, { + onRequestHandlerError: (ids, error, ctx) => { + errors.push({ ids, error, ctx }); + }, + }); + + const frame = encodeWireMessage({ + requestId: "req-err", + payload: { id: 7, value: new Uint8Array([1]) }, + }); + assert.ok(frame.isOk()); + deliver(frame.value); + await new Promise((r) => setImmediate(r)); + + assert.equal(errors.length, 1, "onRequestHandlerError fired once"); + assert.equal(errors[0].error.message, "handler boom"); + assert.deepEqual(errors[0].ids, { request: 7, response: 8 }); + assert.equal(errors[0].ctx.requestId, "req-err"); + assert.equal(sent.length, 0, "no response frame on handler rejection"); + + server.dispose(); +}); + +test("a frame with an unregistered id triggers onUnknownFrame", () => { + const { provider, sent, deliver } = makeRecordingProvider(); + + const unknown = []; + const server = createHostServer(provider, [], { + onUnknownFrame: (payload) => unknown.push(payload), + }); + + const frame = encodeWireMessage({ + requestId: "req-unknown", + payload: { id: 200, value: new Uint8Array([9, 9]) }, + }); + assert.ok(frame.isOk()); + deliver(frame.value); + + assert.equal(unknown.length, 1, "onUnknownFrame fired once"); + assert.equal(unknown[0].id, 200); + assert.deepEqual(Array.from(unknown[0].value), [9, 9]); + assert.equal(sent.length, 0, "no frame emitted for an unknown id"); + + server.dispose(); +}); + +test("a truncated/garbage buffer is dropped without throwing or sending", () => { + const { provider, sent, deliver } = makeRecordingProvider(); + + const unknown = []; + const handlerErrors = []; + const server = createHostServer(provider, [], { + onUnknownFrame: (payload) => unknown.push(payload), + onRequestHandlerError: (_ids, error) => handlerErrors.push(error), + }); + + // A compact-length prefix that promises more bytes than exist. + assert.doesNotThrow(() => deliver(new Uint8Array([0xff, 0xff, 0xff, 0xff]))); + + assert.equal(sent.length, 0, "no frame emitted for a garbage buffer"); + assert.equal(unknown.length, 0, "decode failure is not an unknown frame"); + assert.equal( + handlerErrors.length, + 0, + "decode failure does not reach handlers", + ); + + server.dispose(); +}); + +test("firing the provider close callback disposes the server", async () => { + const recording = makeRecordingProvider(); + const { provider, sent, deliver, triggerClose } = recording; + + let handled = 0; + const entries = [ + { + kind: "request", + ids: { request: 7, response: 8 }, + async handle(_ctx, payload) { + handled += 1; + return payload; + }, + }, + ]; + const server = createHostServer(provider, entries); + + triggerClose(new Error("provider gone")); + + const frame = encodeWireMessage({ + requestId: "after-close", + payload: { id: 7, value: new Uint8Array([1]) }, + }); + assert.ok(frame.isOk()); + deliver(frame.value); + await new Promise((r) => setImmediate(r)); + + assert.equal(handled, 0, "inbound frames are ignored after close"); + assert.equal(sent.length, 0, "no frames emitted after close"); + + // dispose remains idempotent after a close-driven teardown. + server.dispose(); +}); diff --git a/js/packages/truapi-host-wasm/test/node-wasm-provider.test.mjs b/js/packages/truapi-host-wasm/test/node-wasm-provider.test.mjs new file mode 100644 index 00000000..70354c11 --- /dev/null +++ b/js/packages/truapi-host-wasm/test/node-wasm-provider.test.mjs @@ -0,0 +1,225 @@ +// Smoke test that `createNodeWasmProvider` instantiates the WASM core, +// returns a usable `Provider`, and disposes cleanly without leaking +// resources back to the caller. + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + encodeWireMessage, + decodeWireMessage, + VersionedHostFeatureSupportedRequest, + HostFeatureSupportedResponse, + GenericError, + scale as S, +} from "@parity/truapi"; +import { SYSTEM_FEATURE_SUPPORTED } from "@parity/truapi/wire-table"; + +import { createNodeWasmProvider } from "../dist/index.js"; + +function makeCallbacks(overrides = {}) { + const noopSubscribe = () => () => {}; + return { + navigateTo: async () => {}, + pushNotification: async () => 0, + devicePermission: async () => false, + remotePermission: async () => false, + featureSupported: async () => false, + localStorageRead: async () => undefined, + localStorageWrite: async () => {}, + localStorageClear: async () => {}, + clearSession: async () => {}, + preimageLookupSubscribe: noopSubscribe, + dispose: () => {}, + ...overrides, + }; +} + +function runtimeConfig(overrides = {}) { + return { + productLabel: "dotli", + productId: "dotli.dot", + siteId: "dot.li", + hostName: "Polkadot Web", + hostIcon: "https://dot.li/dotli.png", + hostVersion: "0.5.0", + platformType: "node", + platformVersion: process.versions.node, + peopleChainGenesisHash: + "0xa22a2424d2cbf561eaecf7da8b1b548fa9d1939f60265e942b1049616a012f71", + pairingDeeplinkScheme: "polkadotapp", + ...overrides, + }; +} + +test("createNodeWasmProvider returns a usable Provider", async () => { + const provider = await createNodeWasmProvider(makeCallbacks(), { + runtimeConfig: runtimeConfig(), + }); + assert.equal(typeof provider.postMessage, "function"); + assert.equal(typeof provider.subscribe, "function"); + assert.equal(typeof provider.disconnect, "function"); + assert.equal(typeof provider.dispose, "function"); + + // Subscribe and immediately unsubscribe to exercise the listener + // bookkeeping without needing a valid frame. + const unsubscribe = provider.subscribe(() => {}); + unsubscribe(); + + provider.dispose(); +}); + +test("createNodeWasmProvider exposes core disconnect", async () => { + let clears = 0; + const provider = await createNodeWasmProvider( + makeCallbacks({ + clearSession: async () => { + clears += 1; + }, + }), + { + runtimeConfig: runtimeConfig(), + }, + ); + + await provider.disconnect(); + + assert.equal(clears, 1); + provider.dispose(); +}); + +test("createNodeWasmProvider validates runtimeConfig in the WASM core", async () => { + await assert.rejects( + () => + createNodeWasmProvider(makeCallbacks(), { + runtimeConfig: runtimeConfig({ + peopleChainGenesisHash: "0x1234", + }), + }), + /runtimeConfig\.peopleChainGenesisHash: expected 32-byte hex string/, + ); +}); + +test("createNodeWasmProvider requires runtimeConfig", async () => { + await assert.rejects( + () => createNodeWasmProvider(makeCallbacks(), undefined), + /runtimeConfig is required/, + ); +}); + +test("createNodeWasmProvider rejects empty runtime config identity fields", async () => { + await assert.rejects( + () => + createNodeWasmProvider(makeCallbacks(), { + runtimeConfig: runtimeConfig({ + productId: " ", + }), + }), + /runtimeConfig\.productId must not be empty/, + ); +}); + +test("createNodeWasmProvider rejects non-HTTPS runtime host icons", async () => { + await assert.rejects( + () => + createNodeWasmProvider(makeCallbacks(), { + runtimeConfig: runtimeConfig({ + hostIcon: "http://localhost:3000/dotli.png", + }), + }), + /runtimeConfig\.hostIcon must use https scheme/, + ); +}); + +test("createNodeWasmProvider dispose is idempotent", async () => { + const provider = await createNodeWasmProvider(makeCallbacks(), { + runtimeConfig: runtimeConfig(), + }); + const closes = []; + provider.subscribeClose((error) => closes.push(error)); + provider.dispose(); + // Second call must not throw. + provider.dispose(); + assert.equal(closes.length, 1); + assert.match(closes[0].message, /disposed/); + + let lateClose = null; + provider.subscribeClose((error) => { + lateClose = error; + }); + assert.ok(lateClose instanceof Error); +}); + +test("createNodeWasmProvider round-trips a featureSupported request through the WASM core", async () => { + const callbacks = makeCallbacks(); + callbacks.featureSupported = async () => true; + const provider = await createNodeWasmProvider(callbacks, { + runtimeConfig: runtimeConfig(), + }); + + const frames = []; + provider.subscribe((bytes) => frames.push(bytes)); + + const payload = VersionedHostFeatureSupportedRequest.enc({ + tag: "V1", + value: { tag: "Chain", value: { genesisHash: "0x00" } }, + }); + const inbound = encodeWireMessage({ + requestId: "rt-1", + payload: { id: SYSTEM_FEATURE_SUPPORTED.request, value: payload }, + }); + assert.ok(inbound.isOk(), "request frame must encode"); + provider.postMessage(inbound.value); + + // Let the WASM dispatch + host callback + emit cycle settle. + await new Promise((r) => setTimeout(r, 50)); + + assert.equal(frames.length, 1, "exactly one response frame emitted"); + const decoded = decodeWireMessage(frames[0]); + assert.ok(decoded.isOk(), "response frame must decode"); + assert.equal(decoded.value.requestId, "rt-1"); + assert.equal(decoded.value.payload.id, SYSTEM_FEATURE_SUPPORTED.response); + + const responseCodec = S.indexedTaggedUnion({ + V1: [0, S.Result(HostFeatureSupportedResponse, GenericError)], + }); + const response = responseCodec.dec(decoded.value.payload.value); + assert.deepEqual(response, { + tag: "V1", + value: { success: true, value: { supported: true } }, + }); + + provider.dispose(); +}); + +test("createNodeWasmProvider surfaces a rejected receiveFromProduct through subscribeClose", async () => { + const provider = await createNodeWasmProvider(makeCallbacks(), { + runtimeConfig: runtimeConfig(), + }); + + const closes = []; + provider.subscribeClose((error) => closes.push(error)); + const frames = []; + provider.subscribe((bytes) => frames.push(bytes)); + + // A garbage buffer the core cannot decode rejects receiveFromProduct. + provider.postMessage(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff])); + await new Promise((r) => setTimeout(r, 50)); + + assert.equal(closes.length, 1, "close listener fires once on decode failure"); + assert.ok(closes[0] instanceof Error); + assert.equal(frames.length, 0, "no response frame on a rejected frame"); + + // Close is terminal: further frames are dropped and close fires only once. + provider.postMessage(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff])); + await new Promise((r) => setTimeout(r, 50)); + assert.equal(closes.length, 1, "close listener does not fire again"); + + let lateClose = null; + provider.subscribeClose((error) => { + lateClose = error; + }); + assert.ok(lateClose instanceof Error); + + provider.dispose(); +}); diff --git a/js/packages/truapi-host-wasm/test/preload-bridge.test.mjs b/js/packages/truapi-host-wasm/test/preload-bridge.test.mjs new file mode 100644 index 00000000..68df840e --- /dev/null +++ b/js/packages/truapi-host-wasm/test/preload-bridge.test.mjs @@ -0,0 +1,147 @@ +// Verify `createElectronProvider` adapts an Electron-style port into a +// TrUAPI Provider: subscribers receive inbound binary frames, outbound +// frames flow back through `port.postMessage`, and `dispose` closes the +// port and clears listeners. + +import assert from "node:assert/strict"; +import test from "node:test"; + +import { createElectronProvider } from "../dist/electron/index.js"; + +function makeFakePort() { + const messageListeners = new Set(); + const closeListeners = new Set(); + const sent = []; + const offCalls = []; + let closed = false; + return { + sent, + offCalls, + isClosed: () => closed, + deliverMessage(data) { + for (const listener of [...messageListeners]) listener({ data }); + }, + deliverClose() { + for (const listener of [...closeListeners]) listener(); + }, + api: { + postMessage(message) { + sent.push(message); + }, + on(event, handler) { + if (event === "message") messageListeners.add(handler); + else if (event === "close") closeListeners.add(handler); + return this; + }, + off(event, handler) { + offCalls.push(event); + if (event === "message") messageListeners.delete(handler); + else if (event === "close") closeListeners.delete(handler); + return this; + }, + start() {}, + close() { + closed = true; + }, + }, + }; +} + +test("createElectronProvider forwards inbound frames to subscribers and outbound frames to the port", () => { + const fake = makeFakePort(); + const provider = createElectronProvider({ port: fake.api }); + + const received = []; + const unsubscribe = provider.subscribe((message) => { + received.push(message); + }); + + const inbound = new Uint8Array([10, 20, 30]); + fake.deliverMessage(inbound); + // Non-binary frames are ignored. + fake.deliverMessage({ type: "ignored" }); + + assert.equal(received.length, 1); + assert.deepEqual(Array.from(received[0]), [10, 20, 30]); + + const outbound = new Uint8Array([1, 2]); + provider.postMessage(outbound); + assert.equal(fake.sent.length, 1); + assert.deepEqual(Array.from(fake.sent[0]), [1, 2]); + + unsubscribe(); + fake.deliverMessage(new Uint8Array([99])); + // Unsubscribed, length should not grow. + assert.equal(received.length, 1); + + provider.dispose(); + assert.equal(fake.isClosed(), true, "dispose closes the underlying port"); +}); + +test("createElectronProvider notifies close subscribers when the port closes", () => { + const fake = makeFakePort(); + const provider = createElectronProvider({ port: fake.api }); + + const closes = []; + provider.subscribeClose((error) => closes.push(error)); + + fake.deliverClose(); + assert.equal(closes.length, 1); + assert.ok(closes[0] instanceof Error); + + let lateClose = null; + provider.subscribeClose((error) => { + lateClose = error; + }); + assert.ok(lateClose instanceof Error); + + provider.dispose(); +}); + +test("dispose removes both port listeners and blocks further traffic", () => { + const fake = makeFakePort(); + const provider = createElectronProvider({ port: fake.api }); + + const received = []; + provider.subscribe((message) => received.push(message)); + + provider.dispose(); + let lateClose = null; + provider.subscribeClose((error) => { + lateClose = error; + }); + assert.ok(lateClose instanceof Error); + assert.deepEqual( + [...fake.offCalls].sort(), + ["close", "message"], + "dispose detaches both the message and close handlers", + ); + + // postMessage after dispose is a no-op. + provider.postMessage(new Uint8Array([1, 2])); + assert.equal(fake.sent.length, 0, "no frames sent after dispose"); + + // Inbound frames after dispose never reach subscribers. + fake.deliverMessage(new Uint8Array([3, 4])); + assert.equal(received.length, 0, "no frames delivered after dispose"); +}); + +test("a peer-initiated close detaches port listeners and blocks postMessage", () => { + const fake = makeFakePort(); + const provider = createElectronProvider({ port: fake.api }); + + provider.subscribeClose(() => {}); + fake.deliverClose(); + + assert.deepEqual( + [...fake.offCalls].sort(), + ["close", "message"], + "peer close detaches both handlers", + ); + + // postMessage after a peer-initiated close is a no-op. + provider.postMessage(new Uint8Array([5])); + assert.equal(fake.sent.length, 0, "no frames sent after a peer close"); + + provider.dispose(); +}); diff --git a/js/packages/truapi-host-wasm/test/typed-callbacks.test.mjs b/js/packages/truapi-host-wasm/test/typed-callbacks.test.mjs new file mode 100644 index 00000000..bfea6719 --- /dev/null +++ b/js/packages/truapi-host-wasm/test/typed-callbacks.test.mjs @@ -0,0 +1,269 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + HostDevicePermissionRequest, + HostFeatureSupportedRequest, + HostPushNotificationRequest, + RemotePermissionRequest, +} from "@parity/truapi"; + +import { + createUnavailableCallbacks, + createWasmRawCallbacks, +} from "../dist/index.js"; + +const GENESIS = `0x${"11".repeat(32)}`; + +function settle() { + return new Promise((resolve) => setImmediate(resolve)); +} + +test("createUnavailableCallbacks rejects storage write paths", async () => { + const callbacks = createUnavailableCallbacks(); + + await assert.rejects( + () => callbacks.localStorageWrite("key", new Uint8Array([1])), + /localStorageWrite unavailable/, + ); + await assert.rejects( + () => callbacks.localStorageClear("key"), + /localStorageClear unavailable/, + ); + assert.equal(await callbacks.localStorageRead("key"), undefined); +}); + +test("createWasmRawCallbacks decodes SCALE request callbacks into typed host calls", async () => { + const writes = []; + const clears = []; + const cancelled = []; + const raw = createWasmRawCallbacks({ + pushNotification: async (notification) => ({ + id: notification.text.length, + }), + cancelNotification: async (id) => { + cancelled.push(id); + }, + devicePermission: async (request) => ({ granted: request === "Camera" }), + remotePermission: async (request) => ({ + granted: request.permission.tag === "ChainSubmit", + }), + featureSupported: async (request) => ({ + supported: + request.tag === "Chain" && request.value.genesisHash === GENESIS, + }), + read: async (key) => new TextEncoder().encode(`read:${key}`), + write: async (key, value) => { + writes.push([key, [...value]]); + }, + clear: async (key) => { + clears.push(key); + }, + }); + + assert.equal( + await raw.pushNotification( + HostPushNotificationRequest.enc({ + text: "hello", + deeplink: undefined, + scheduledAt: undefined, + }), + ), + 5, + ); + assert.equal( + await raw.devicePermission(HostDevicePermissionRequest.enc("Camera")), + true, + ); + assert.equal( + await raw.remotePermission( + RemotePermissionRequest.enc({ permission: { tag: "ChainSubmit" } }), + ), + true, + ); + assert.equal( + await raw.featureSupported( + HostFeatureSupportedRequest.enc({ + tag: "Chain", + value: { genesisHash: GENESIS }, + }), + ), + true, + ); + assert.deepEqual( + await raw.localStorageRead("session"), + new TextEncoder().encode("read:session"), + ); + + await raw.localStorageWrite("session", new Uint8Array([1, 2, 3])); + await raw.localStorageClear("session"); + await raw.cancelNotification?.(9); + + assert.deepEqual(writes, [["session", [1, 2, 3]]]); + assert.deepEqual(clears, ["session"]); + assert.deepEqual(cancelled, [9]); +}); + +test("createWasmRawCallbacks bridges lifecycle, confirmations, and preimage callbacks", async () => { + const calls = []; + async function* sessionTicks() { + yield { success: true, value: undefined }; + yield { success: true, value: undefined }; + } + async function* preimages() { + yield { success: true, value: undefined }; + yield { success: true, value: new Uint8Array([4, 5, 6]) }; + } + + const raw = createWasmRawCallbacks({ + authStateChanged: (state) => { + calls.push(["authStateChanged", state]); + }, + readSession: async () => new Uint8Array([1, 2, 3]), + writeSession: async (value) => { + calls.push(["writeSession", [...value]]); + }, + clearSession: async () => { + calls.push(["clearSession"]); + }, + subscribeSessionStore: () => sessionTicks(), + confirmSignPayload: async (payload) => payload[0] === 1, + confirmSignRaw: async (payload) => payload[0] === 2, + confirmCreateTransaction: async (payload) => payload[0] === 3, + confirmAccountAlias: async (payload) => payload[0] === 4, + confirmResourceAllocation: async (payload) => payload[0] === 5, + confirmPreimageSubmit: async (size) => { + calls.push(["confirmPreimageSubmit", size]); + }, + submitPreimage: async (value) => { + calls.push(["submitPreimage", [...value]]); + return new Uint8Array([7, 8, 9]); + }, + lookupPreimage: (key) => { + calls.push(["lookupPreimage", [...key]]); + return preimages(); + }, + }); + + const sessionEvents = []; + const disposeSession = raw.subscribeSessionStore?.(() => + sessionEvents.push("tick"), + ); + const preimageEvents = []; + const disposePreimages = raw.preimageLookupSubscribe( + new Uint8Array([9]), + (value) => preimageEvents.push(value ? [...value] : null), + ); + + raw.authStateChanged?.({ + tag: "Pairing", + value: { deeplink: "polkadotapp://example" }, + }); + assert.deepEqual(await raw.readSession?.(), new Uint8Array([1, 2, 3])); + await raw.writeSession?.(new Uint8Array([3, 2, 1])); + await raw.clearSession?.(); + assert.equal(await raw.confirmSignPayload?.(new Uint8Array([1])), true); + assert.equal(await raw.confirmSignRaw?.(new Uint8Array([2])), true); + assert.equal(await raw.confirmCreateTransaction?.(new Uint8Array([3])), true); + assert.equal(await raw.confirmAccountAlias?.(new Uint8Array([4])), true); + assert.equal( + await raw.confirmResourceAllocation?.(new Uint8Array([5])), + true, + ); + await raw.confirmPreimageSubmit(42); + assert.deepEqual( + await raw.submitPreimage(new Uint8Array([6])), + new Uint8Array([7, 8, 9]), + ); + + await settle(); + await settle(); + + assert.deepEqual(sessionEvents, ["tick", "tick"]); + assert.deepEqual(preimageEvents, [null, [4, 5, 6]]); + assert.deepEqual(calls, [ + ["lookupPreimage", [9]], + [ + "authStateChanged", + { tag: "Pairing", value: { deeplink: "polkadotapp://example" } }, + ], + ["writeSession", [3, 2, 1]], + ["clearSession"], + ["confirmPreimageSubmit", 42n], + ["submitPreimage", [6]], + ]); + + disposeSession?.(); + disposePreimages?.(); +}); + +test("createWasmRawCallbacks default session-store subscription emits current tick", () => { + const raw = createWasmRawCallbacks({}); + const ticks = []; + raw.subscribeSessionStore?.(() => ticks.push("tick")); + assert.deepEqual(ticks, ["tick"]); +}); + +test("createWasmRawCallbacks default theme and preimage subscriptions emit current values", () => { + const raw = createWasmRawCallbacks({}); + const themes = []; + raw.themeSubscribe?.((theme) => themes.push(theme)); + assert.deepEqual(themes, ["Dark"]); + + const preimages = []; + raw.preimageLookupSubscribe(new Uint8Array([1]), (value) => + preimages.push(value), + ); + assert.deepEqual(preimages, [undefined]); +}); + +test("createWasmRawCallbacks adapts typed result subscriptions", async () => { + async function* themes() { + yield { success: true, value: "Dark" }; + yield { success: true, value: "Light" }; + } + + const raw = createWasmRawCallbacks({ + subscribeTheme: () => themes(), + }); + const seen = []; + const dispose = raw.themeSubscribe?.((theme) => seen.push(theme)); + + await settle(); + await settle(); + + assert.deepEqual(seen, ["Dark", "Light"]); + dispose?.(); +}); + +test("createWasmRawCallbacks bridges typed chain connections", async () => { + const sent = []; + const responses = ['{"jsonrpc":"2.0","id":1,"result":"ok"}']; + const raw = createWasmRawCallbacks({ + connect: async (genesisHash) => { + assert.deepEqual([...genesisHash], Array(32).fill(0x11)); + return { + send(request) { + sent.push(request); + }, + async *responses() { + yield* responses; + }, + }; + }, + }); + + assert.equal(typeof raw.chainConnect, "function"); + const received = []; + const connection = await raw.chainConnect?.(GENESIS, (json) => + received.push(json), + ); + assert.ok(connection); + + connection.send('{"jsonrpc":"2.0","id":1,"method":"system_health"}'); + await settle(); + + assert.deepEqual(sent, ['{"jsonrpc":"2.0","id":1,"method":"system_health"}']); + assert.deepEqual(received, responses); + connection.close(); +}); diff --git a/js/packages/truapi-host-wasm/test/worker-protocol.test.mjs b/js/packages/truapi-host-wasm/test/worker-protocol.test.mjs new file mode 100644 index 00000000..ece703d8 --- /dev/null +++ b/js/packages/truapi-host-wasm/test/worker-protocol.test.mjs @@ -0,0 +1,33 @@ +// Sanity test that the worker-protocol module is importable and exports +// what `createWebWorkerProvider` (from @parity/truapi-host-wasm/web) expects. The +// real web worker entry-point loads a browser-only WASM bundle, so we +// cannot boot it under Node; this test verifies the wire-shape of the +// shared protocol contract instead. + +import assert from "node:assert/strict"; +import test from "node:test"; + +import * as shared from "../dist/index.js"; +import * as workerProtocol from "../dist/worker-protocol.js"; + +test("worker-protocol exports callback-name tables", () => { + assert.deepEqual(Object.keys(workerProtocol), [ + "CALLBACK_NAMES", + "OPTIONAL_CALLBACK_NAMES", + ]); + assert.ok(workerProtocol.CALLBACK_NAMES.includes("navigateTo")); + assert.ok(workerProtocol.OPTIONAL_CALLBACK_NAMES.includes("readSession")); + assert.ok(!workerProtocol.OPTIONAL_CALLBACK_NAMES.includes("navigateTo")); +}); + +test("@parity/truapi-host-wasm exposes the documented surface", () => { + // Dispatcher re-export from @parity/truapi-host. + assert.equal(typeof shared.createHostServer, "function"); + assert.equal(typeof shared.toFlatResponsePayload, "function"); + assert.equal(typeof shared.toResponsePayload, "function"); + + // WASM provider helpers. + assert.equal(typeof shared.createWasmProvider, "function"); + assert.equal(typeof shared.createNodeWasmProvider, "function"); + assert.equal(typeof shared.createUnavailableCallbacks, "function"); +}); diff --git a/js/packages/truapi-host-wasm/test/worker-provider.test.mjs b/js/packages/truapi-host-wasm/test/worker-provider.test.mjs new file mode 100644 index 00000000..6cf6f753 --- /dev/null +++ b/js/packages/truapi-host-wasm/test/worker-provider.test.mjs @@ -0,0 +1,591 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { HostPushNotificationRequest } from "../../truapi/dist/index.js"; +import { createWasmRawCallbacks } from "../dist/index.js"; +import { createWebWorkerProvider } from "../dist/web/index.js"; + +class FakeWorker { + constructor() { + this.listeners = new Map(); + this.messages = []; + this.terminated = false; + } + + addEventListener(name, fn) { + const listeners = this.listeners.get(name) ?? new Set(); + listeners.add(fn); + this.listeners.set(name, listeners); + } + + removeEventListener(name, fn) { + this.listeners.get(name)?.delete(fn); + } + + postMessage(message) { + this.messages.push(message); + } + + terminate() { + this.terminated = true; + } + + emit(message) { + for (const listener of this.listeners.get("message") ?? []) { + listener({ data: message }); + } + } + + emitError(message) { + for (const listener of this.listeners.get("error") ?? []) { + listener({ message }); + } + } + + emitMessageError() { + for (const listener of this.listeners.get("messageerror") ?? []) { + listener({ data: null }); + } + } +} + +function makeCallbacks(overrides = {}) { + return { + navigateTo: async () => {}, + pushNotification: async () => 0, + devicePermission: async () => false, + remotePermission: async () => false, + featureSupported: async () => false, + localStorageRead: async () => undefined, + localStorageWrite: async () => {}, + localStorageClear: async () => {}, + ...overrides, + }; +} + +function runtimeConfig(overrides = {}) { + return { + productLabel: "dotli", + productId: "dotli.dot", + siteId: "dot.li", + hostName: "Polkadot Web", + hostIcon: "https://dot.li/dotli.png", + hostVersion: "0.5.0", + platformType: "node", + platformVersion: process.versions.node, + peopleChainGenesisHash: + "0xa22a2424d2cbf561eaecf7da8b1b548fa9d1939f60265e942b1049616a012f71", + pairingDeeplinkScheme: "polkadotapp", + ...overrides, + }; +} + +async function settle() { + await new Promise((resolve) => setImmediate(resolve)); +} + +async function readyProvider(worker, options = {}) { + const { + createWebWorkerProvider: createProvider = createWebWorkerProvider, + ...providerOptions + } = options; + const providerPromise = createProvider(worker, makeCallbacks(), { + runtimeConfig: runtimeConfig(), + ...providerOptions, + }); + worker.emit({ kind: "loaded" }); + worker.emit({ kind: "ready" }); + return providerPromise; +} + +test("createWebWorkerProvider advertises only supplied optional hooks", async () => { + const worker = new FakeWorker(); + const config = runtimeConfig(); + const providerPromise = createWebWorkerProvider( + worker, + makeCallbacks({ + clearSession: async () => {}, + readSession: async () => new Uint8Array([1]), + authStateChanged: () => {}, + subscribeSessionStore: () => () => {}, + preimageLookupSubscribe: () => () => {}, + chainConnect: () => ({ send: () => {}, close: () => {} }), + }), + { + logLevel: "debug", + runtimeConfig: config, + }, + ); + + worker.emit({ kind: "loaded" }); + assert.equal(worker.messages.length, 1); + assert.deepEqual(worker.messages[0], { + kind: "init", + logLevel: "debug", + runtimeConfig: config, + optionalCallbacks: ["authStateChanged", "readSession", "clearSession"], + optionalSubscriptions: ["sessionStoreSubscribe", "preimageLookupSubscribe"], + chainConnect: true, + }); + + worker.emit({ kind: "ready" }); + const provider = await providerPromise; + assert.equal(typeof provider.disconnect, "function"); + assert.equal(typeof provider.cancelLogin, "function"); + + provider.dispose(); +}); + +test("dev global setLogLevel updates every live worker provider", async () => { + const previous = globalThis.__truapi; + delete globalThis.__truapi; + const firstWorker = new FakeWorker(); + const secondWorker = new FakeWorker(); + const first = await readyProvider(firstWorker); + const second = await readyProvider(secondWorker); + + globalThis.__truapi.setLogLevel("debug"); + + assert.deepEqual(firstWorker.messages.at(-1), { + kind: "setLogLevel", + level: "debug", + }); + assert.deepEqual(secondWorker.messages.at(-1), { + kind: "setLogLevel", + level: "debug", + }); + assert.equal(globalThis.__truapi.getLogLevel(), "debug"); + assert.equal(globalThis.__truapi.getProviderCount(), 2); + + globalThis.__truapi.setLogLevel("off"); + first.dispose(); + second.dispose(); + if (previous === undefined) { + delete globalThis.__truapi; + } else { + globalThis.__truapi = previous; + } +}); + +test("dev global setLogLevel applies to providers created later", async () => { + const previous = globalThis.__truapi; + delete globalThis.__truapi; + const moduleUrl = `../dist/web/create-worker-host-runtime.js?dev-global-${Date.now()}`; + const { createWebWorkerProvider: freshCreateWebWorkerProvider } = + await import(moduleUrl); + + assert.equal(typeof globalThis.__truapi.setLogLevel, "function"); + assert.equal(globalThis.__truapi.getProviderCount(), 0); + globalThis.__truapi.setLogLevel("trace"); + + const firstWorker = new FakeWorker(); + const first = await readyProvider(firstWorker, { + createWebWorkerProvider: freshCreateWebWorkerProvider, + }); + first.dispose(); + + const secondWorker = new FakeWorker(); + const second = await readyProvider(secondWorker, { + createWebWorkerProvider: freshCreateWebWorkerProvider, + }); + + assert.equal(secondWorker.messages[0].kind, "init"); + assert.equal(secondWorker.messages[0].logLevel, "trace"); + assert.deepEqual(secondWorker.messages.at(-1), { + kind: "setLogLevel", + level: "trace", + }); + + second.dispose(); + globalThis.__truapi.setLogLevel("off"); + if (previous === undefined) { + delete globalThis.__truapi; + } else { + globalThis.__truapi = previous; + } +}); + +test("dev global setLogLevel persists the level to localStorage", async () => { + const previousGlobal = globalThis.__truapi; + const previousStorage = globalThis.localStorage; + delete globalThis.__truapi; + const store = new Map(); + globalThis.localStorage = { + getItem: (key) => (store.has(key) ? store.get(key) : null), + setItem: (key, value) => store.set(key, String(value)), + }; + + const worker = new FakeWorker(); + const provider = await readyProvider(worker); + + globalThis.__truapi.setLogLevel("debug"); + assert.equal(store.get("truapi:logLevel"), "debug"); + + globalThis.__truapi.setLogLevel("off"); + assert.equal(store.get("truapi:logLevel"), "off"); + + provider.dispose(); + globalThis.localStorage = previousStorage; + if (previousGlobal === undefined) { + delete globalThis.__truapi; + } else { + globalThis.__truapi = previousGlobal; + } +}); + +test("worker provider resolves disconnect responses", async () => { + const worker = new FakeWorker(); + const providerPromise = createWebWorkerProvider(worker, makeCallbacks(), { + runtimeConfig: runtimeConfig(), + }); + worker.emit({ kind: "loaded" }); + worker.emit({ kind: "ready" }); + const provider = await providerPromise; + + const disconnect = provider.disconnect(); + const msg = worker.messages.at(-1); + assert.equal(msg.kind, "disconnect"); + assert.equal(typeof msg.requestId, "number"); + + worker.emit({ + kind: "disconnectResponse", + requestId: msg.requestId, + ok: true, + }); + await disconnect; + + provider.dispose(); +}); + +test("worker provider dispatches optional callback requests to host hooks", async () => { + const worker = new FakeWorker(); + let clears = 0; + const providerPromise = createWebWorkerProvider( + worker, + makeCallbacks({ + clearSession: async () => { + clears += 1; + }, + }), + { + runtimeConfig: runtimeConfig(), + }, + ); + worker.emit({ kind: "loaded" }); + worker.emit({ kind: "ready" }); + const provider = await providerPromise; + + worker.emit({ + kind: "callbackRequest", + requestId: 7, + name: "clearSession", + args: [], + }); + await settle(); + + assert.equal(clears, 1); + assert.deepEqual(worker.messages.at(-1), { + kind: "callbackResponse", + requestId: 7, + ok: true, + value: undefined, + }); + + provider.dispose(); +}); + +test("worker provider forwards authStateChanged callback requests", async () => { + const worker = new FakeWorker(); + const states = []; + const providerPromise = createWebWorkerProvider( + worker, + makeCallbacks({ + authStateChanged: (state) => { + states.push(state); + }, + }), + { + runtimeConfig: runtimeConfig(), + }, + ); + worker.emit({ kind: "loaded" }); + worker.emit({ kind: "ready" }); + const provider = await providerPromise; + + worker.emit({ + kind: "callbackRequest", + requestId: 3, + name: "authStateChanged", + args: [ + { + tag: "Connected", + value: { + connected: true, + publicKey: new Uint8Array([1, 2]), + liteUsername: "alice", + }, + }, + ], + }); + await settle(); + + assert.deepEqual(states, [ + { + tag: "Connected", + value: { + connected: true, + publicKey: new Uint8Array([1, 2]), + liteUsername: "alice", + }, + }, + ]); + assert.deepEqual(worker.messages.at(-1), { + kind: "callbackResponse", + requestId: 3, + ok: true, + value: undefined, + }); + + provider.dispose(); +}); + +test("worker provider posts cancelLogin to the worker", async () => { + const worker = new FakeWorker(); + const provider = await readyProvider(worker); + + provider.cancelLogin(); + + assert.deepEqual(worker.messages.at(-1), { kind: "cancelLogin" }); + provider.dispose(); +}); + +test("worker fault terminates the worker and runs the full teardown", async () => { + const worker = new FakeWorker(); + let subscriptionDisposes = 0; + let chainCloses = 0; + const providerPromise = createWebWorkerProvider( + worker, + makeCallbacks({ + subscribeSessionStore: () => () => { + subscriptionDisposes += 1; + }, + chainConnect: () => ({ + send: () => {}, + close: () => { + chainCloses += 1; + }, + }), + }), + { runtimeConfig: runtimeConfig() }, + ); + worker.emit({ kind: "loaded" }); + worker.emit({ kind: "ready" }); + const provider = await providerPromise; + + worker.emit({ + kind: "subscriptionStart", + subId: 1, + name: "sessionStoreSubscribe", + payload: null, + }); + worker.emit({ kind: "chainConnectStart", connId: 1, genesisHash: "0xab" }); + await settle(); + + const closes = []; + provider.subscribeClose((error) => closes.push(error)); + + worker.emitError("boom"); + + assert.equal(worker.terminated, true); + assert.equal(subscriptionDisposes, 1); + assert.equal(chainCloses, 1); + assert.equal(closes.length, 1); + assert.match(closes[0].message, /boom/); + + // The fault teardown is terminal; a second fault is a no-op. + worker.emitError("again"); + assert.equal(closes.length, 1); + + let lateClose = null; + provider.subscribeClose((error) => { + lateClose = error; + }); + assert.ok(lateClose instanceof Error); + assert.match(lateClose.message, /boom/); +}); + +test("worker fatalError during init rejects provider creation", async () => { + const worker = new FakeWorker(); + const providerPromise = createWebWorkerProvider(worker, makeCallbacks(), { + runtimeConfig: runtimeConfig(), + }); + + worker.emit({ kind: "fatalError", error: "bad wasm" }); + + await assert.rejects(providerPromise, /worker init reported error: bad wasm/); + assert.equal(worker.terminated, true); +}); + +test("worker frameError after init closes the provider", async () => { + const worker = new FakeWorker(); + const provider = await readyProvider(worker); + const closes = []; + provider.subscribeClose((error) => closes.push(error)); + + worker.emit({ kind: "frameError", error: "bad frame" }); + + assert.equal(worker.terminated, true); + assert.equal(closes.length, 1); + assert.match(closes[0].message, /worker frame error: bad frame/); + + let lateClose = null; + provider.subscribeClose((error) => { + lateClose = error; + }); + assert.ok(lateClose instanceof Error); +}); + +test("worker provider routes payload-carrying subscriptions by name", async () => { + const worker = new FakeWorker(); + const keys = []; + let push; + const providerPromise = createWebWorkerProvider( + worker, + makeCallbacks({ + preimageLookupSubscribe: (key, sendItem) => { + keys.push(key); + push = sendItem; + return () => {}; + }, + }), + { runtimeConfig: runtimeConfig() }, + ); + worker.emit({ kind: "loaded" }); + worker.emit({ kind: "ready" }); + const provider = await providerPromise; + + worker.emit({ + kind: "subscriptionStart", + subId: 4, + name: "preimageLookupSubscribe", + payload: new Uint8Array([9, 9]), + }); + + assert.deepEqual(keys, [new Uint8Array([9, 9])]); + push(new Uint8Array([1])); + assert.deepEqual(worker.messages.at(-1), { + kind: "subscriptionItem", + subId: 4, + value: new Uint8Array([1]), + }); + + provider.dispose(); +}); + +test("unknown subscription names never fall through to another callback", async () => { + const worker = new FakeWorker(); + let preimageStarts = 0; + const providerPromise = createWebWorkerProvider( + worker, + makeCallbacks({ + preimageLookupSubscribe: () => { + preimageStarts += 1; + return () => {}; + }, + }), + { runtimeConfig: runtimeConfig() }, + ); + worker.emit({ kind: "loaded" }); + worker.emit({ kind: "ready" }); + const provider = await providerPromise; + + worker.emit({ + kind: "subscriptionStart", + subId: 5, + name: "someFutureSubscribe", + payload: new Uint8Array([1, 2, 3]), + }); + + assert.equal(preimageStarts, 0); + assert.equal( + worker.messages.some((m) => m.kind === "subscriptionItem"), + false, + ); + + provider.dispose(); +}); + +test("payload-carrying subscription without payload is not dispatched", async () => { + const worker = new FakeWorker(); + let preimageStarts = 0; + const providerPromise = createWebWorkerProvider( + worker, + makeCallbacks({ + preimageLookupSubscribe: () => { + preimageStarts += 1; + return () => {}; + }, + }), + { runtimeConfig: runtimeConfig() }, + ); + worker.emit({ kind: "loaded" }); + worker.emit({ kind: "ready" }); + const provider = await providerPromise; + + worker.emit({ + kind: "subscriptionStart", + subId: 6, + name: "preimageLookupSubscribe", + payload: null, + }); + + assert.equal(preimageStarts, 0); + + provider.dispose(); +}); + +test("createWebWorkerProvider rejects when init times out", async () => { + const worker = new FakeWorker(); + const providerPromise = createWebWorkerProvider(worker, makeCallbacks(), { + runtimeConfig: runtimeConfig(), + initTimeoutMs: 20, + }); + worker.emit({ kind: "loaded" }); + await assert.rejects(providerPromise, /worker init timed out after 20ms/); + assert.equal(worker.terminated, true); +}); + +test("createWebWorkerProvider rejects on messageerror during init", async () => { + const worker = new FakeWorker(); + const providerPromise = createWebWorkerProvider(worker, makeCallbacks(), { + runtimeConfig: runtimeConfig(), + }); + worker.emitMessageError(); + await assert.rejects(providerPromise, /could not be deserialized/); + assert.equal(worker.terminated, true); +}); + +test("typed callbacks decode raw v01 push notification payloads", async () => { + let notification; + const callbacks = createWasmRawCallbacks({ + pushNotification: async (request) => { + notification = request; + return { id: 42 }; + }, + }); + + const id = await callbacks.pushNotification( + HostPushNotificationRequest.enc({ + text: "Hello!", + deeplink: undefined, + scheduledAt: undefined, + }), + ); + + assert.equal(id, 42); + assert.deepEqual(notification, { + text: "Hello!", + deeplink: undefined, + scheduledAt: undefined, + }); +}); diff --git a/js/packages/truapi-host-wasm/tsconfig.json b/js/packages/truapi-host-wasm/tsconfig.json new file mode 100644 index 00000000..8a51c612 --- /dev/null +++ b/js/packages/truapi-host-wasm/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "composite": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "esModuleInterop": true, + "skipLibCheck": true, + "lib": ["ES2022", "DOM", "WebWorker"] + }, + "include": ["src"], + "references": [{ "path": "../truapi" }, { "path": "../truapi-host" }] +} diff --git a/js/packages/truapi-host/package.json b/js/packages/truapi-host/package.json index 343626e7..4e131ab6 100644 --- a/js/packages/truapi-host/package.json +++ b/js/packages/truapi-host/package.json @@ -29,7 +29,7 @@ }, "scripts": { "ensure-generated": "./scripts/ensure-generated.sh", - "build": "tsc", + "build": "tsc -b", "prebuild": "npm run ensure-generated", "codegen": "cargo run -p truapi-codegen -- --input ../../../target/doc/truapi.json --output ../truapi/src/generated --host-output src/generated", "typecheck": "npm run build", diff --git a/js/packages/truapi-host/test/result-subscription.test.mjs b/js/packages/truapi-host/test/result-subscription.test.mjs index 1bd12952..d15ae3e5 100644 --- a/js/packages/truapi-host/test/result-subscription.test.mjs +++ b/js/packages/truapi-host/test/result-subscription.test.mjs @@ -94,7 +94,7 @@ await new Promise((resolveTest, rejectTest) => { const server = createTrUApiServer(b, makeStubHandlers({ payment: paymentStub })); const received = []; - client.payment.balanceSubscribe().subscribe({ + client.payment.balanceSubscribe({ request: {} }).subscribe({ next(item) { received.push(item); }, diff --git a/js/packages/truapi-host/test/versioned-roundtrip.test.mjs b/js/packages/truapi-host/test/versioned-roundtrip.test.mjs index cd69a91c..9569bf02 100644 --- a/js/packages/truapi-host/test/versioned-roundtrip.test.mjs +++ b/js/packages/truapi-host/test/versioned-roundtrip.test.mjs @@ -118,8 +118,7 @@ function makeStubHandlers(partial) { const transport = createTransport(a); const client = createClient(transport); - const sampleRequest = { - account: { dotNsIdentifier: "alice.dot", derivationIndex: 0 }, + const samplePayload = { blockHash: "0x" + "00".repeat(32), blockNumber: "0x10", era: "0x00", @@ -132,6 +131,10 @@ function makeStubHandlers(partial) { signedExtensions: [], version: 4, }; + const sampleRequest = { + account: { dotNsIdentifier: "alice.dot", derivationIndex: 0 }, + payload: samplePayload, + }; const sampleSignature = "0x" + "ab".repeat(32); let observedRequest; @@ -155,11 +158,11 @@ function makeStubHandlers(partial) { assert.equal(result.value.signature, sampleSignature); // SCALE decode reifies optional fields, so compare key-by-key on the // input set rather than deepEqual on the whole shape. - for (const [key, expected] of Object.entries(sampleRequest)) { + for (const [key, expected] of Object.entries(samplePayload)) { assert.deepEqual( - observedRequest.value[key], + observedRequest.value.payload[key], expected, - `signPayload request.value.${key} mismatch`, + `signPayload request.value.payload.${key} mismatch`, ); } diff --git a/js/packages/truapi-host/tsconfig.json b/js/packages/truapi-host/tsconfig.json index 71725ac1..0238f57b 100644 --- a/js/packages/truapi-host/tsconfig.json +++ b/js/packages/truapi-host/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2022", "module": "ES2022", "moduleResolution": "bundler", + "composite": true, "declaration": true, "outDir": "dist", "rootDir": "src", @@ -12,5 +13,6 @@ "esModuleInterop": true, "skipLibCheck": true }, - "include": ["src"] + "include": ["src"], + "references": [{ "path": "../truapi" }] } diff --git a/js/packages/truapi/README.md b/js/packages/truapi/README.md index 06aa67fd..34be9f17 100644 --- a/js/packages/truapi/README.md +++ b/js/packages/truapi/README.md @@ -77,7 +77,7 @@ Frames are SCALE encoded: [requestId: SCALE str][discriminant: u8][payload bytes...] ``` -The discriminant table is generated from Rust `#[wire(request_id = N)]` and `#[wire(start_id = N)]` annotations and is written to `src/generated/wire-table.ts`. +The discriminant table is generated from Rust `#[wire(request_id = N)]` and `#[wire(start_id = N)]` annotations and is written to `src/generated/wire-table.ts`. The package also exports a reverse lookup for debug labels: `WIRE_TAG_BY_ID` (discriminant → `_` tag) and `describeWireId(id)` (falls back to `wire_` for unknown discriminants). ## Generated files diff --git a/js/packages/truapi/package.json b/js/packages/truapi/package.json index 46ae9d73..befc9fb6 100644 --- a/js/packages/truapi/package.json +++ b/js/packages/truapi/package.json @@ -62,7 +62,7 @@ }, "scripts": { "ensure-generated": "./scripts/ensure-generated.sh", - "build": "tsc", + "build": "tsc -b", "prebuild": "npm run ensure-generated", "codegen": "cargo run -p truapi-codegen -- --input ../../../target/doc/truapi.json --output src/generated --playground-output src/playground --explorer-output src/explorer", "typecheck": "npm run build", @@ -73,6 +73,7 @@ "typescript": "^6.0" }, "dependencies": { + "@noble/hashes": "^2.2.0", "neverthrow": "^8.2.0", "scale-ts": "^1.6.1" } diff --git a/js/packages/truapi/src/index.ts b/js/packages/truapi/src/index.ts index bc6bcef2..99411af8 100644 --- a/js/packages/truapi/src/index.ts +++ b/js/packages/truapi/src/index.ts @@ -12,14 +12,17 @@ export type { TrUApiTransport, } from "./transport.js"; export type { CreateTransportOptions } from "./client.js"; +export type { WebSocketProviderOptions } from "./transport.js"; export { SubscriptionError, createIframeProvider, createMessagePortProvider, + createWebSocketProvider, decodeWireMessage, encodeWireMessage, } from "./transport.js"; export { createTransport } from "./client.js"; +export { WIRE_TAG_BY_ID, describeWireId } from "./wire-tags.js"; export * as scale from "./scale.js"; export type { Codec, HexString } from "./scale.js"; export * from "./generated/index.js"; diff --git a/js/packages/truapi/src/scale.ts b/js/packages/truapi/src/scale.ts index ac92039b..f0d1cc89 100644 --- a/js/packages/truapi/src/scale.ts +++ b/js/packages/truapi/src/scale.ts @@ -11,9 +11,14 @@ import { createCodec, createDecoder, enhanceCodec, + str as scaleStr, u8, type Codec, } from "scale-ts"; +import { + bytesToHex as encodeHex, + hexToBytes as decodeHex, +} from "@noble/hashes/utils.js"; export type { Codec }; export type { ResultPayload } from "scale-ts"; @@ -79,22 +84,12 @@ export function toHexString(value: string): HexString { /** Encode a byte array as a lower-case hex string with a `0x` prefix. */ export function bytesToHex(bytes: Uint8Array): HexString { - let hex = "0x"; - for (let i = 0; i < bytes.length; i++) { - hex += bytes[i]!.toString(16).padStart(2, "0"); - } - return hex as HexString; + return `0x${encodeHex(bytes)}`; } /** Decode a hex string into a byte array. Tolerates a missing `0x` prefix. */ export function hexToBytes(hex: string): Uint8Array { - const start = hex.startsWith("0x") ? 2 : 0; - const length = (hex.length - start) >> 1; - const bytes = new Uint8Array(length); - for (let i = 0; i < length; i++) { - bytes[i] = parseInt(hex.substring(start + i * 2, start + i * 2 + 2), 16); - } - return bytes; + return decodeHex(hex.startsWith("0x") ? hex.slice(2) : hex); } /** @@ -123,6 +118,44 @@ export function TaggedUnion( return Enum(inner) as unknown as Codec>; } +/** + * Wire codec for Rust `CallError`, projected to the public domain error `D`. + * + * Generated TypeScript APIs expose only the domain error union in + * `ResultAsync`. The Rust host still wraps that value in + * `CallError::Domain` on the wire so framework errors can share the response + * channel. Encoding always emits `Domain`; decoding returns the inner domain + * value and throws for framework-level failures that have no public `D` shape. + */ +export function CallError(domain: Codec): Codec { + return createCodec( + (value: D) => { + const payload = domain.enc(value); + const out = new Uint8Array(payload.length + 1); + out[0] = 0; + out.set(payload, 1); + return out; + }, + createDecoder((input) => { + const tag = u8.dec(input); + switch (tag) { + case 0: + return domain.dec(input); + case 1: + throw new Error("Host denied the request"); + case 2: + throw new Error("Host does not support this request"); + case 3: + throw new Error(`Malformed request frame: ${scaleStr.dec(input)}`); + case 4: + throw new Error(`Host failure: ${scaleStr.dec(input)}`); + default: + throw new Error(`Unknown CallError discriminant: ${tag}`); + } + }), + ); +} + type TaggedUnionCodecs = { [Sym: symbol]: never; [Num: number]: never; diff --git a/js/packages/truapi/src/transport.ts b/js/packages/truapi/src/transport.ts index 8a92133e..a60c3737 100644 --- a/js/packages/truapi/src/transport.ts +++ b/js/packages/truapi/src/transport.ts @@ -9,6 +9,12 @@ function toError(error: unknown): Error { return error instanceof Error ? error : new Error(String(error)); } +function toWebSocketPayload(message: Uint8Array): Uint8Array { + const copy: Uint8Array = new Uint8Array(message.byteLength); + copy.set(message); + return copy; +} + /** * Handle returned by TrUAPI subscription APIs. **/ @@ -277,6 +283,10 @@ export interface Provider { /** * Register a callback for provider-level close or failure events. + * + * Providers keep a terminal close reason. The callback fires at most once + * for an active subscription, and fires immediately when registered after + * the provider has already closed. **/ subscribeClose?(callback: (error: Error) => void): () => void; @@ -580,3 +590,102 @@ export function createMessagePortProvider( }, }; } + +/** + * Options accepted by `createWebSocketProvider`. + **/ +export interface WebSocketProviderOptions { + /** + * Override the `WebSocket` constructor. Useful for non-browser runtimes + * (Node, tests) where the global isn't available. + **/ + WebSocket?: typeof WebSocket; +} + +/** + * Create a provider backed by a binary WebSocket. Used by products that + * connect through the native host's localhost WS bridge, the host exposes + * an endpoint shaped like `ws://127.0.0.1:/?t=` and the product + * passes that URL straight to this constructor. + **/ +export function createWebSocketProvider( + url: string, + options: WebSocketProviderOptions = {}, +): Provider { + const WebSocketCtor = options.WebSocket ?? globalThis.WebSocket; + if (!WebSocketCtor) { + throw new Error("WebSocket constructor not available in this environment"); + } + + const base = createBaseProvider(); + const socket = new WebSocketCtor(url); + socket.binaryType = "arraybuffer"; + + const pending: Uint8Array[] = []; + base.onClose(() => { + pending.length = 0; + }); + + socket.onopen = () => { + for (const msg of pending) { + try { + socket.send(toWebSocketPayload(msg)); + } catch (error) { + base.close(error); + return; + } + } + pending.length = 0; + }; + + socket.onmessage = (event: MessageEvent) => { + const data = event.data; + if (!(data instanceof ArrayBuffer)) { + return; + } + base.deliver(new Uint8Array(data)); + }; + + socket.onerror = () => { + base.close(new Error("websocket error")); + }; + + socket.onclose = (event: CloseEvent) => { + base.close( + new Error( + `websocket closed (code=${event.code}, reason=${event.reason || "unknown"})`, + ), + ); + }; + + return { + postMessage(message) { + const error = base.closed(); + if (error) { + throw error; + } + if (socket.readyState === WebSocketCtor.OPEN) { + try { + socket.send(toWebSocketPayload(message)); + } catch (error) { + base.close(error); + throw toError(error); + } + } else if (socket.readyState === WebSocketCtor.CONNECTING) { + pending.push(message); + } else { + throw new Error("websocket not open"); + } + }, + subscribe: base.subscribe, + subscribeClose: base.subscribeClose, + dispose() { + base.close(new Error("websocket provider disposed")); + try { + socket.close(); + } catch { + // ignore duplicate close during shutdown + } + }, + }; +} diff --git a/js/packages/truapi/src/wire-tags.ts b/js/packages/truapi/src/wire-tags.ts new file mode 100644 index 00000000..8fa6d20a --- /dev/null +++ b/js/packages/truapi/src/wire-tags.ts @@ -0,0 +1,32 @@ +// Reverse lookup over the generated wire table, used to label wire frames +// in debug logs. The frame-kind keys (`request`, `start`, ...) are derived +// from the table rows themselves rather than hardcoded. + +import * as WireTable from "./generated/wire-table.js"; + +/** + * Frame tag (`_`, e.g. `system_handshake_request`) keyed by + * wire discriminant, precomputed from the generated wire table. + **/ +export const WIRE_TAG_BY_ID: ReadonlyMap = (() => { + const map = new Map(); + for (const [name, row] of Object.entries(WireTable)) { + if (typeof row !== "object" || row === null) { + continue; + } + for (const [kind, id] of Object.entries(row)) { + if (typeof id === "number") { + map.set(id, `${name.toLowerCase()}_${kind}`); + } + } + } + return map; +})(); + +/** + * Human-readable label for a wire discriminant; `wire_` when the id is + * not in the wire table. + **/ +export function describeWireId(id: number): string { + return WIRE_TAG_BY_ID.get(id) ?? `wire_${String(id)}`; +} diff --git a/js/packages/truapi/test/ws-provider.test.mjs b/js/packages/truapi/test/ws-provider.test.mjs new file mode 100644 index 00000000..0775e051 --- /dev/null +++ b/js/packages/truapi/test/ws-provider.test.mjs @@ -0,0 +1,279 @@ +// Smoke test for `createWebSocketProvider` against a stubbed WebSocket +// constructor. Verifies the open/queue lifecycle, the close fan-out, and +// that double-registration of the same listener is independent (Set semantics). + +import assert from "node:assert/strict"; + +import { createWebSocketProvider } from "../dist/transport.js"; + +function makeStubWebSocket(opts = {}) { + // Records what the provider did to its socket so the tests can assert. + const sent = []; + let openHandler = null; + let messageHandler = null; + let closeHandler = null; + let errorHandler = null; + let readyState = 0; // CONNECTING + let sendThrows = opts.sendThrows ?? false; + + class StubWebSocket { + static get CONNECTING() { + return 0; + } + static get OPEN() { + return 1; + } + static get CLOSING() { + return 2; + } + static get CLOSED() { + return 3; + } + + binaryType = ""; + + set onopen(fn) { + openHandler = fn; + } + set onmessage(fn) { + messageHandler = fn; + } + set onclose(fn) { + closeHandler = fn; + } + set onerror(fn) { + errorHandler = fn; + } + + get readyState() { + return readyState; + } + + send(bytes) { + if (sendThrows) throw new Error("send failed"); + sent.push(bytes); + } + close() { + readyState = 3; + if (closeHandler) closeHandler({ code: 1000, reason: "" }); + } + } + + return { + StubWebSocket, + sent, + open() { + readyState = 1; + if (openHandler) openHandler(); + }, + setReadyState(state) { + readyState = state; + }, + setSendThrows(value) { + sendThrows = value; + }, + deliver(data) { + if (messageHandler) { + messageHandler({ data }); + } + }, + inbound(bytes) { + if (messageHandler) { + messageHandler({ data: bytes.buffer }); + } + }, + triggerClose(code, reason) { + readyState = 3; + if (closeHandler) closeHandler({ code, reason }); + }, + triggerError() { + if (errorHandler) errorHandler(); + }, + }; +} + +// 1. queues outbound while connecting; flushes on open +{ + const stub = makeStubWebSocket(); + const provider = createWebSocketProvider("ws://127.0.0.1:0/?t=token", { + WebSocket: stub.StubWebSocket, + }); + + provider.postMessage(new Uint8Array([1, 2, 3])); + provider.postMessage(new Uint8Array([4, 5])); + assert.equal(stub.sent.length, 0, "nothing sent while CONNECTING"); + + stub.open(); + assert.deepEqual( + stub.sent.map((b) => Array.from(b)), + [ + [1, 2, 3], + [4, 5], + ], + "queued frames flush in order on open", + ); +} + +// 2. fan-out: every active listener receives every inbound frame +{ + const stub = makeStubWebSocket(); + const provider = createWebSocketProvider("ws://127.0.0.1:0/?t=token", { + WebSocket: stub.StubWebSocket, + }); + stub.open(); + + const received = []; + const a = (bytes) => received.push(["a", Array.from(bytes)]); + const b = (bytes) => received.push(["b", Array.from(bytes)]); + const unsubA = provider.subscribe(a); + provider.subscribe(b); + + stub.inbound(new Uint8Array([0xaa])); + unsubA(); + stub.inbound(new Uint8Array([0xbb])); + + assert.deepEqual(received, [ + ["a", [0xaa]], + ["b", [0xaa]], + ["b", [0xbb]], + ]); +} + +// 3. subscribe is set-based: re-registering the same callback is idempotent +{ + const stub = makeStubWebSocket(); + const provider = createWebSocketProvider("ws://127.0.0.1:0/?t=token", { + WebSocket: stub.StubWebSocket, + }); + stub.open(); + + let count = 0; + const cb = () => { + count += 1; + }; + const unsub1 = provider.subscribe(cb); + const unsub2 = provider.subscribe(cb); + stub.inbound(new Uint8Array([1])); + assert.equal(count, 1, "duplicate registration counts as one listener"); + + unsub1(); + unsub2(); + stub.inbound(new Uint8Array([2])); + assert.equal(count, 1, "unsubscribed listener is silent"); +} + +// 4. subscribeClose fires once on socket close; late subscribers see the stored error +{ + const stub = makeStubWebSocket(); + const provider = createWebSocketProvider("ws://127.0.0.1:0/?t=token", { + WebSocket: stub.StubWebSocket, + }); + stub.open(); + + const errors = []; + provider.subscribeClose((err) => errors.push(err)); + stub.triggerClose(1006, "abnormal"); + assert.equal(errors.length, 1); + assert.match(errors[0].message, /websocket closed/); + + let late = null; + provider.subscribeClose((err) => { + late = err; + }); + assert.equal(late, errors[0], "late subscriber receives the stored close error"); +} + +// 5. postMessage after close throws the stored error +{ + const stub = makeStubWebSocket(); + const provider = createWebSocketProvider("ws://127.0.0.1:0/?t=token", { + WebSocket: stub.StubWebSocket, + }); + stub.open(); + stub.triggerClose(1000, ""); + assert.throws( + () => provider.postMessage(new Uint8Array([1])), + /websocket closed/, + ); +} + +// 6. triggerError() surfaces a /websocket error/ through subscribeClose +{ + const stub = makeStubWebSocket(); + const provider = createWebSocketProvider("ws://127.0.0.1:0/?t=token", { + WebSocket: stub.StubWebSocket, + }); + stub.open(); + + const errors = []; + provider.subscribeClose((err) => errors.push(err)); + stub.triggerError(); + assert.equal(errors.length, 1); + assert.match(errors[0].message, /websocket error/); +} + +// 7. non-ArrayBuffer inbound payloads are dropped without firing listeners +{ + const stub = makeStubWebSocket(); + const provider = createWebSocketProvider("ws://127.0.0.1:0/?t=token", { + WebSocket: stub.StubWebSocket, + }); + stub.open(); + + let count = 0; + provider.subscribe(() => { + count += 1; + }); + stub.deliver("not-an-arraybuffer"); + stub.deliver({ some: "object" }); + assert.equal(count, 0, "non-ArrayBuffer frames are ignored"); + + // A real ArrayBuffer still flows through. + stub.inbound(new Uint8Array([7])); + assert.equal(count, 1, "ArrayBuffer frames still deliver"); +} + +// 8. postMessage while readyState is CLOSING (2) throws /websocket not open/ +{ + const stub = makeStubWebSocket(); + const provider = createWebSocketProvider("ws://127.0.0.1:0/?t=token", { + WebSocket: stub.StubWebSocket, + }); + stub.open(); + stub.setReadyState(2); // CLOSING + assert.throws( + () => provider.postMessage(new Uint8Array([1])), + /websocket not open/, + ); +} + +// 9. a send that throws during the onopen flush closes the provider +{ + const stub = makeStubWebSocket(); + const provider = createWebSocketProvider("ws://127.0.0.1:0/?t=token", { + WebSocket: stub.StubWebSocket, + }); + + const errors = []; + provider.subscribeClose((err) => errors.push(err)); + + // Queue a frame while CONNECTING, then make the socket throw on send and + // open it so the flush hits the failing send path. + provider.postMessage(new Uint8Array([1, 2, 3])); + stub.setSendThrows(true); + stub.open(); + + assert.equal( + errors.length, + 1, + "flush failure surfaces through subscribeClose", + ); + assert.match(errors[0].message, /send failed/); + assert.throws( + () => provider.postMessage(new Uint8Array([4])), + /send failed/, + "provider is closed after the failed flush", + ); +} + +console.log("createWebSocketProvider tests passed"); diff --git a/js/packages/truapi/tsconfig.json b/js/packages/truapi/tsconfig.json index 71725ac1..2b8ac495 100644 --- a/js/packages/truapi/tsconfig.json +++ b/js/packages/truapi/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2022", "module": "ES2022", "moduleResolution": "bundler", + "composite": true, "declaration": true, "outDir": "dist", "rootDir": "src", diff --git a/package-lock.json b/package-lock.json index a194a5b2..50a7efb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "version": "0.3.0", "license": "MIT", "dependencies": { + "@noble/hashes": "^2.2.0", "neverthrow": "^8.2.0", "scale-ts": "^1.6.1" }, @@ -41,6 +42,18 @@ "typescript": "^5.7" } }, + "js/packages/truapi-host-wasm": { + "name": "@parity/truapi-host-wasm", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@parity/truapi": "file:../truapi", + "@parity/truapi-host": "file:../truapi-host" + }, + "devDependencies": { + "typescript": "^5.7" + } + }, "js/packages/truapi/node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -433,6 +446,18 @@ "node": ">=6 <7 || >=8" } }, + "node_modules/@noble/hashes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.2.0.tgz", + "integrity": "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -479,6 +504,10 @@ "resolved": "js/packages/truapi-host", "link": true }, + "node_modules/@parity/truapi-host-wasm": { + "resolved": "js/packages/truapi-host-wasm", + "link": true + }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.60.3", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", diff --git a/playground/src/components/DiagnosisView.tsx b/playground/src/components/DiagnosisView.tsx index 2c401efd..842588e8 100644 --- a/playground/src/components/DiagnosisView.tsx +++ b/playground/src/components/DiagnosisView.tsx @@ -72,13 +72,17 @@ export function DiagnosisView({ }; }, [services, testResults]); + const reportMarkdown = useMemo( + () => + hasResults && !isRunning + ? renderReportMarkdown(services, testResults) + : "", + [hasResults, isRunning, services, testResults], + ); + const handleCopyReport = async () => { try { - // Rendered on demand: the full report is only needed on copy, not on - // every per-method result update during a run. - await navigator.clipboard.writeText( - renderReportMarkdown(services, testResults), - ); + await navigator.clipboard.writeText(reportMarkdown); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch { @@ -92,7 +96,7 @@ export function DiagnosisView({ // Copy the report to the clipboard first as a fallback if the body is // truncated. const handleSubmitReport = () => { - const report = renderReportMarkdown(services, testResults); + const report = reportMarkdown; void navigator.clipboard?.writeText(report).catch(() => {}); const url = reportIssueUrl(report, detectHostMode()); try { @@ -123,11 +127,12 @@ export function DiagnosisView({ About

- Runs every TrUAPI method against the connected host to build a coverage - report — which methods work, which fail, and which aren't wired - yet. Methods run one at a time, in order; those that need your approval - (signing, permission and resource requests) wait on your response - before the run continues. When it finishes, copy the report below. + Runs every TrUAPI method against the connected host to build a + coverage report — which methods work, which fail, and which + aren't wired yet. Methods run one at a time, in order; those that + need your approval (signing, permission and resource requests) wait on + your response before the run continues. When it finishes, copy the + report below.

Before you start: make sure you are logged in, and @@ -145,7 +150,12 @@ export function DiagnosisView({ Stop ) : ( - @@ -153,6 +163,7 @@ export function DiagnosisView({ {hasResults && ( 0} > {passCount} success · {failCount} failed @@ -160,9 +171,17 @@ export function DiagnosisView({ )} {hasResults && !isRunning && (

+